novas funcionalidades dos formulario
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-02 11:13:37 +02:00
parent a6afb4e97a
commit 4f35313d06
104 changed files with 7653 additions and 178 deletions
+9
View File
@@ -66,6 +66,15 @@ VITE_APP_NAME="${APP_NAME}"
# Comma-separated emails of verified accounts authorised to manage the portal.
PORTAL_MANAGER_EMAILS=
# Explicit, separate application workflow designations. Empty means no access.
INVESTMOZ_MNE_TRIAGERS=
INVESTMOZ_MNE_REVIEWERS=
INVESTMOZ_MNE_APPROVERS_L1=
INVESTMOZ_MNE_APPROVERS_L2=
INVESTMOZ_MNE_PUBLISHERS=
INVESTMOZ_CONSENT_POLICY_VERSION=2026-09-30
INVESTMOZ_SHARING_POLICY_VERSION=2026-09-30
INVESTMOZ_SLA_ESCALATION_ENABLED=false
PORTAL_NOTIFICATIONS_ENABLED=false
PORTAL_NOTIFICATION_RECIPIENTS=
PORTAL_SUPPORT_EMAIL=
+864
View File
@@ -0,0 +1,864 @@
openapi: 3.1.0
info:
title: InvestMoz API
version: 1.0.0
summary: Contrato consolidado da API do InvestMoz
description: |
Contrato consolidado para catálogos, oportunidades, manifestações de interesse,
simulações, legislação, jobs, relatórios e administração. Valores de taxas,
reduções e parâmetros de simulação são fornecidos pelo utilizador; não são
taxas oficiais nem decisões de elegibilidade.
contact:
name: Equipa InvestMoz
license:
name: MIT
identifier: MIT
servers:
- url: /
description: Mesmo host da aplicação
security:
- sessionCookie: []
tags:
- name: Catálogos
description: Referências versionadas, estáveis e localizadas em pt-MZ.
- name: Oportunidades
description: Consulta pública de oportunidades de investimento.
- name: Manifestações
description: Submissão de interesse em oportunidades.
- name: Simulações
description: Execuções indicativas com snapshots e controlo de concorrência.
- name: Jobs
description: Estado dos trabalhos assíncronos associados a uma simulação.
- name: Relatórios
description: Leitura de snapshots imutáveis de relatórios.
- name: Legislação
description: Fontes legais publicadas e versionadas.
- name: Administração
description: Operações administrativas protegidas por sessão e autorização.
paths:
/api/v1/catalogs/{catalog}:
get:
tags: [Catálogos]
operationId: listReferenceCatalog
summary: Consultar catálogo versionado
security: []
parameters:
- $ref: '#/components/parameters/CatalogName'
- $ref: '#/components/parameters/Version'
- $ref: '#/components/parameters/CatalogType'
- $ref: '#/components/parameters/ParentId'
- $ref: '#/components/parameters/JurisdictionId'
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Página do catálogo na versão solicitada
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/CatalogPage'}
'404': {$ref: '#/components/responses/NotFound'}
'422': {$ref: '#/components/responses/ValidationError'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/questionnaires/{code}:
get:
tags: [Simulações]
operationId: getPublishedQuestionnaire
summary: Consultar questionário publicado e versão imutável
security: []
parameters:
- $ref: '#/components/parameters/QuestionnaireCode'
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Questionário publicado em pt-MZ
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/QuestionnaireResponse'}
'404': {$ref: '#/components/responses/NotFound'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/simulations:
post:
tags: [Simulações]
operationId: createSimulation
summary: Criar simulação rastreável
description: Requer Idempotency-Key. Acesso anónimo requer token opaco gerado pelo cliente.
x-implementation-status: implemented
security:
- sessionCookie: []
- {}
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/SimulationToken'
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/SimulationInput'}
examples:
BoundaryValues:
summary: Exemplo técnico nos limites mínimos
description: Valores mínimos para demonstrar a forma do payload; não são valores fiscais recomendados.
value: {base: 0, rate: 0, reduction: 0}
responses:
'202':
description: Simulação aceite para processamento
headers:
ETag: {$ref: '#/components/headers/ETag'}
Location: {$ref: '#/components/headers/Location'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/SimulationAccepted'}
'200':
description: Repetição idempotente da resposta original
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/SimulationAccepted'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/simulations/{id}:
get:
tags: [Simulações]
operationId: getSimulation
summary: Consultar simulação, cenários e estado de execução
x-implementation-status: implemented
security:
- sessionCookie: []
- {}
parameters:
- $ref: '#/components/parameters/SimulationId'
- $ref: '#/components/parameters/SimulationToken'
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Simulação dentro do âmbito autorizado
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/SimulationResponse'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/simulations/{id}/scenarios/{scenarioId}:
patch:
tags: [Simulações]
operationId: updateSimulationScenario
summary: Actualizar cenário com controlo optimista de concorrência
x-implementation-status: implemented
security:
- sessionCookie: []
- {}
parameters:
- $ref: '#/components/parameters/SimulationId'
- $ref: '#/components/parameters/ScenarioId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/SimulationToken'
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/UpdateScenarioInput'}
responses:
'202':
description: Alteração aceite e reexecução colocada em fila
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/Scenario'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'412': {$ref: '#/components/responses/PreconditionFailed'}
'422': {$ref: '#/components/responses/ValidationError'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/simulations/{id}/jobs:
get:
tags: [Jobs]
operationId: listSimulationJobs
summary: Consultar jobs de execução e geração de relatórios
description: Contrato reservado para consulta dos jobs associados à simulação.
x-implementation-status: contract-only
security:
- sessionCookie: []
- {}
parameters:
- $ref: '#/components/parameters/SimulationId'
- $ref: '#/components/parameters/SimulationToken'
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Jobs visíveis no âmbito autorizado
headers:
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/JobCollection'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/simulations/{id}/reports/{reportId}:
get:
tags: [Relatórios]
operationId: getSimulationReport
summary: Obter snapshot imutável de relatório
x-implementation-status: implemented
security:
- sessionCookie: []
- {}
parameters:
- $ref: '#/components/parameters/SimulationId'
- $ref: '#/components/parameters/ReportId'
- $ref: '#/components/parameters/SimulationToken'
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Snapshot e hash do relatório
headers:
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/ReportResponse'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/legislation/sources:
get:
tags: [Legislação]
operationId: listLegislationSources
summary: Consultar fontes legais publicadas e versionadas
description: Contrato reservado para catálogo público de fontes; administração usa a API v2 existente.
x-implementation-status: contract-only
security: []
parameters:
- $ref: '#/components/parameters/Page'
- $ref: '#/components/parameters/PerPage'
- $ref: '#/components/parameters/Search'
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Fontes legais publicadas
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/LegislationPage'}
'404': {$ref: '#/components/responses/NotFound'}
'422': {$ref: '#/components/responses/ValidationError'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v2/investment/admin/legal-sources:
get:
tags: [Administração, Legislação]
operationId: adminListLegalSources
summary: Listar fontes legais para administração
x-implementation-status: implemented
security: [{sessionCookie: []}]
parameters:
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Lista de fontes legais
headers:
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/AdminLegalSourcePage'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
post:
tags: [Administração, Legislação]
operationId: adminCreateLegalSource
summary: Registar fonte legal para validação
x-implementation-status: implemented
security: [{sessionCookie: []}]
parameters:
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/LegalSourceInput'}
responses:
'201':
description: Fonte legal registada
headers:
Location: {$ref: '#/components/headers/Location'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/LegalSource'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v2/investment/admin/legal-sources/{id}/{action}:
post:
tags: [Administração, Legislação]
operationId: adminTransitionLegalSource
summary: Validar ou revogar uma fonte legal
x-implementation-status: implemented
security: [{sessionCookie: []}]
parameters:
- $ref: '#/components/parameters/SourceId'
- name: action
in: path
required: true
schema: {type: string, enum: [validate, revoke]}
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/LegalSourceTransitionInput'}
responses:
'200':
description: Estado actualizado
headers:
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/LegalSource'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/interest-requests:
post:
tags: [Manifestações]
operationId: submitInterestRequest
summary: Submeter manifestação de interesse
x-implementation-status: implemented
security: []
parameters:
- $ref: '#/components/parameters/IdempotencyKeyOptional'
- $ref: '#/components/parameters/CorrelationId'
requestBody:
required: true
content:
application/json:
schema: {$ref: '#/components/schemas/InterestRequestInput'}
responses:
'201':
description: Manifestação submetida
headers:
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/InterestRequestResponse'}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
/api/v1/opportunities/{slug}:
get:
tags: [Oportunidades]
operationId: getOpportunity
summary: Consultar oportunidade publicada
x-implementation-status: implemented
security: []
parameters:
- name: slug
in: path
required: true
schema: {type: string, minLength: 1, maxLength: 180}
- $ref: '#/components/parameters/CorrelationId'
responses:
'200':
description: Oportunidade publicada
headers:
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/OpportunityResponse'}
'404': {$ref: '#/components/responses/NotFound'}
'429': {$ref: '#/components/responses/RateLimited'}
'500': {$ref: '#/components/responses/ServerError'}
components:
securitySchemes:
sessionCookie:
type: apiKey
in: cookie
name: laravel-session
description: Sessão Laravel; o nome pode ser substituído por SESSION_COOKIE. Rotas públicas e simulações anónimas substituem explicitamente este requisito.
parameters:
CatalogName:
name: catalog
in: path
required: true
schema: {type: string, enum: [authorities, jurisdictions, economic-activities]}
Version:
name: version
in: query
required: false
description: Versão estável publicada do catálogo
schema: {type: integer, minimum: 1}
CatalogType:
name: type
in: query
required: false
description: Obrigatório para jurisdictions.
schema: {type: string, enum: [country, province, district, admin_post, locality]}
ParentId:
name: parent_id
in: query
schema: {type: string, format: uuid}
JurisdictionId:
name: jurisdiction_id
in: query
schema: {type: string, format: uuid}
Page:
name: page
in: query
schema: {type: integer, minimum: 1, default: 1}
PerPage:
name: per_page
in: query
schema: {type: integer, minimum: 1, maximum: 100, default: 20}
Search:
name: q
in: query
schema: {type: string, maxLength: 100}
QuestionnaireCode:
name: code
in: path
required: true
schema: {type: string, pattern: '^[a-z0-9][a-z0-9-]{0,79}$'}
SimulationId:
name: id
in: path
required: true
schema: {type: string, format: uuid}
ScenarioId:
name: scenarioId
in: path
required: true
schema: {type: string, format: uuid}
ReportId:
name: reportId
in: path
required: true
schema: {type: string, format: uuid}
SourceId:
name: id
in: path
required: true
schema: {type: string, format: uuid}
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
description: Repetições com a mesma chave e payload devolvem a resposta original; payload diferente produz conflito.
schema: {type: string, minLength: 16, maxLength: 128}
IdempotencyKeyOptional:
name: Idempotency-Key
in: header
required: false
description: Se fornecida, deve ser UUID; uma chave repetida com outro payload produz conflito.
schema: {type: string, format: uuid}
SimulationToken:
name: X-Simulation-Token
in: header
required: false
description: Obrigatório para acesso anónimo; token opaco do cliente, guardado apenas como hash.
schema: {type: string, minLength: 32, maxLength: 256}
IfMatch:
name: If-Match
in: header
required: true
description: ETag forte da representação lida previamente.
schema: {type: string, minLength: 1, maxLength: 128}
CorrelationId:
name: X-Correlation-ID
in: header
required: false
description: UUID opcional propagado pela aplicação; quando ausente, a aplicação cria um identificador.
schema: {type: string, format: uuid}
headers:
ETag:
description: Validador da representação; usado com If-Match em alterações concorrentes.
schema: {type: string}
Location:
description: URI relativa do recurso criado ou aceite.
schema: {type: string, format: uri-reference}
RetryAfter:
description: Segundos até à próxima tentativa.
schema: {type: integer, minimum: 0}
CorrelationId:
description: Identificador de correlação da operação.
schema: {type: string, format: uuid}
responses:
Unauthorized:
description: Autenticação necessária ou sessão inválida
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}
Forbidden:
description: Actor autenticado sem autorização para a operação
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}
NotFound:
description: Recurso inexistente ou fora do âmbito visível
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}
Conflict:
description: Conflito de estado, versão ou chave idempotente
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content:
application/json:
schema: {$ref: '#/components/schemas/Problem'}
example: {message: A chave foi reutilizada com conteúdo diferente., code: idempotency_conflict}
PreconditionFailed:
description: If-Match não corresponde ao ETag actual
headers:
ETag: {$ref: '#/components/headers/ETag'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content:
application/json:
schema: {$ref: '#/components/schemas/Problem'}
example: {message: A representação mudou; volte a consultar o recurso., code: stale_etag}
ValidationError:
description: Dados inválidos
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/ValidationProblem'}}}
RateLimited:
description: Limite de pedidos excedido
headers:
Retry-After: {$ref: '#/components/headers/RetryAfter'}
X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}
content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}
ServerError:
description: Erro interno; resposta não contém detalhes sensíveis
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}
PreconditionRequired:
description: Header obrigatório em falta, como If-Match ou Idempotency-Key
headers: {X-Correlation-ID: {$ref: '#/components/headers/CorrelationId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/Problem'}}}
schemas:
Problem:
type: object
required: [message]
properties:
message: {type: string}
code: {type: string}
correlation_id: {type: string, format: uuid}
additionalProperties: true
ValidationProblem:
allOf:
- $ref: '#/components/schemas/Problem'
- type: object
properties:
errors:
type: object
additionalProperties:
type: array
items: {type: string}
CatalogItem:
type: object
required: [id, value, code, name, label, locale, version_no]
properties:
id: {type: string, format: uuid}
value: {type: string}
code: {type: string}
name: {type: string, description: Rótulo apresentado em português de Moçambique.}
label: {type: string}
locale: {type: string, const: pt-MZ}
parent_id: {type: [string, 'null']}
type: {type: [string, 'null'], enum: [country, province, district, admin_post, locality, 'null']}
authority_type: {type: [string, 'null']}
organization_id: {type: [string, 'null'], format: uuid}
jurisdiction_id: {type: [string, 'null'], format: uuid}
jurisdiction_name: {type: [string, 'null']}
source_code: {type: [string, 'null']}
sector_id: {type: [string, 'null']}
version_no: {type: integer}
CatalogPage:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/CatalogItem'}}
links:
type: object
properties:
next: {type: [string, 'null'], format: uri-reference}
previous: {type: [string, 'null'], format: uri-reference}
next_page_url: {type: [string, 'null'], format: uri-reference}
prev_page_url: {type: [string, 'null'], format: uri-reference}
current_page: {type: integer}
per_page: {type: integer}
total: {type: integer}
last_page: {type: integer}
meta:
allOf:
- $ref: '#/components/schemas/Pagination'
- type: object
properties:
catalog: {type: string}
version: {type: [integer, 'null']}
locale: {type: string, const: pt-MZ}
source_version: {type: [string, 'null']}
status: {type: string, enum: [published, not_published]}
Pagination:
type: object
required: [current_page, per_page, total]
properties:
current_page: {type: integer, minimum: 1}
per_page: {type: integer, minimum: 1}
total: {type: integer, minimum: 0}
version: {type: string}
QuestionnaireResponse:
type: object
required: [data]
properties:
data:
type: object
required: [id, code, version_no, locale, schema_snapshot, status]
properties:
id: {type: string, format: uuid}
code: {type: string}
version_no: {type: integer, minimum: 1}
locale: {type: string, const: pt-MZ}
schema_snapshot: {type: object, additionalProperties: true}
status: {type: string, const: published}
valid_from: {type: [string, 'null'], format: date}
valid_to: {type: [string, 'null'], format: date}
SimulationInput:
type: object
required: [base, rate]
properties:
base: {type: number, minimum: 0, description: Valor de entrada sem moeda inferida.}
rate: {type: number, minimum: 0, maximum: 100, description: Parâmetro fornecido pelo utilizador; não representa taxa oficial.}
reduction: {type: number, minimum: 0, maximum: 100, default: 0}
organization_id: {type: [string, 'null'], format: uuid}
additionalProperties: false
UpdateScenarioInput:
type: object
required: [label, base, rate]
properties:
label: {type: string, minLength: 1, maxLength: 100}
base: {type: number, minimum: 0}
rate: {type: number, minimum: 0, maximum: 100}
reduction: {type: number, minimum: 0, maximum: 100, default: 0}
additionalProperties: false
SimulationAccepted:
type: object
required: [data, replayed]
properties:
data: {$ref: '#/components/schemas/Simulation'}
replayed: {type: boolean}
access_token: {type: string, minLength: 32, maxLength: 256, description: Devolvido apenas para execução anónima.}
SimulationResponse:
type: object
required: [data]
properties:
data: {$ref: '#/components/schemas/Simulation'}
Simulation:
type: object
required: [id, status, version_no, created_at, updated_at]
properties:
id: {type: string, format: uuid}
questionnaire_version_id: {type: [string, 'null'], format: uuid}
organization_id: {type: [string, 'null'], format: uuid}
version_no: {type: integer, minimum: 1}
status: {type: string, enum: [queued, running, completed, failed, cancelled]}
input_snapshot: {type: object, additionalProperties: true}
result_snapshot: {type: [object, 'null'], additionalProperties: true}
calculation_snapshot: {type: [object, 'null'], additionalProperties: true}
scenarios: {type: array, items: {$ref: '#/components/schemas/Scenario'}}
created_at: {type: string, format: date-time}
updated_at: {type: string, format: date-time}
Scenario:
type: object
required: [id, code, label, version_no, parameters]
properties:
id: {type: string, format: uuid}
code: {type: string}
label: {type: string}
version_no: {type: integer, minimum: 1}
parameters: {type: object, additionalProperties: true}
JobCollection:
type: object
required: [data]
properties:
data: {type: array, items: {$ref: '#/components/schemas/SimulationJob'}}
SimulationJob:
type: object
required: [id, simulation_id, job_type, status, attempts, available_at]
properties:
id: {type: string, format: uuid}
simulation_id: {type: string, format: uuid}
job_type: {type: string, enum: [execute, report]}
status: {type: string, enum: [pending, running, succeeded, failed]}
attempts: {type: integer, minimum: 0, maximum: 3}
available_at: {type: string, format: date-time}
started_at: {type: [string, 'null'], format: date-time}
finished_at: {type: [string, 'null'], format: date-time}
error_code: {type: [string, 'null']}
ReportResponse:
type: object
required: [data]
properties:
data: {$ref: '#/components/schemas/SimulationReport'}
SimulationReport:
type: object
required: [id, simulation_id, version_no, status]
properties:
id: {type: string, format: uuid}
simulation_id: {type: string, format: uuid}
version_no: {type: integer, minimum: 1}
status: {type: string, enum: [queued, ready, failed]}
report_snapshot: {type: [object, 'null'], additionalProperties: true}
report_sha256: {type: [string, 'null'], pattern: '^[0-9a-f]{64}$'}
LegislationPage:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/LegalSource'}}
meta: {$ref: '#/components/schemas/Pagination'}
AdminLegalSourcePage:
type: object
required: [data]
properties:
data: {type: array, items: {$ref: '#/components/schemas/LegalSource'}}
current_page: {type: integer}
per_page: {type: integer}
total: {type: integer}
last_page: {type: integer}
first_page_url: {type: [string, 'null'], format: uri-reference}
last_page_url: {type: [string, 'null'], format: uri-reference}
next_page_url: {type: [string, 'null'], format: uri-reference}
prev_page_url: {type: [string, 'null'], format: uri-reference}
LegalSourceInput:
type: object
required: [title, url, diploma, article, competent_entity, valid_from]
properties:
title: {type: string, minLength: 1, maxLength: 255}
url: {type: string, format: uri, pattern: '^https://', maxLength: 2000}
diploma: {type: string, maxLength: 255}
article: {type: string, maxLength: 255}
competent_entity: {type: string, maxLength: 255}
valid_from: {type: string, format: date}
valid_until: {type: [string, 'null'], format: date}
additionalProperties: false
LegalSourceTransitionInput:
type: object
required: [version_no]
properties:
version_no: {type: integer, minimum: 1}
additionalProperties: false
LegalSource:
type: object
properties:
id: {type: string, format: uuid}
title: {type: string}
url: {type: string, format: uri}
diploma: {type: string}
article: {type: string}
competent_entity: {type: string}
validation_status: {type: string, enum: [pending, validated, revoked]}
version_no: {type: integer}
valid_until: {type: [string, 'null'], format: date}
validator_id: {type: [integer, 'null']}
validated_at: {type: [string, 'null'], format: date-time}
valid_from: {type: [string, 'null'], format: date}
InterestRequestInput:
type: object
required: [fullName, email, investorType, country, sectorInterest, message, privacyConsent]
properties:
fullName: {type: string, minLength: 3, maxLength: 150, writeOnly: true}
email: {type: string, format: email, maxLength: 150, writeOnly: true}
phone: {type: [string, 'null'], maxLength: 30, writeOnly: true}
investorType: {type: string, enum: [individual, empresa, fundo, entidade_publica, outro]}
country: {type: string, maxLength: 100}
sectorInterest: {type: string, maxLength: 150}
opportunityReference: {type: [string, 'null'], maxLength: 80}
existingBusinessId: {type: [string, 'null'], format: uuid}
modality: {type: [string, 'null'], maxLength: 50}
investmentRange: {type: [string, 'null'], maxLength: 80}
message: {type: string, minLength: 20, maxLength: 2000, writeOnly: true}
privacyConsent: {type: boolean, const: true}
shareWithProponent: {type: boolean, default: false}
additionalProperties: false
not:
allOf:
- required: [opportunityReference]
properties:
opportunityReference: {type: string, minLength: 1}
- required: [existingBusinessId]
properties:
existingBusinessId: {type: string, format: uuid}
allOf:
- if:
required: [existingBusinessId]
properties:
existingBusinessId: {type: string}
then:
required: [modality]
properties:
modality: {type: string, minLength: 1, maxLength: 50}
InterestRequestResponse:
type: object
required: [success, message, data]
properties:
success: {type: boolean, const: true}
message: {type: string}
data:
type: object
required: [reference, status]
properties:
reference: {type: string}
status: {type: string, const: received}
OpportunityResponse:
type: object
required: [success, data]
properties:
success: {type: boolean, const: true}
data:
type: object
required: [id, slug, title]
properties:
id: {type: [string, integer]}
slug: {type: string}
title: {type: string}
summary: {type: [string, 'null']}
status: {type: string}
@@ -0,0 +1,30 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class EscalateOverdueInvestmentTasks extends Command
{
protected $signature = 'investmoz:escalate-overdue-tasks';
protected $description = 'Eleva a prioridade de tarefas operacionais vencidas, quando activado.';
public function handle(): int
{
if (! config('existing-business.sla_escalation_enabled')) {
$this->components->info('Escalada SLA desactivada.');
return self::SUCCESS;
}
$count = DB::table('tasks')->whereIn('task_type', ['triage', 'interest_follow_up'])
->whereIn('status', ['open', 'in_progress'])->whereNotNull('due_at')->where('due_at', '<', now())
->where('priority', '>', 1)->update(['priority' => 1, 'outcome' => 'sla_overdue', 'updated_at' => now()]);
$this->components->info("Tarefas elevadas para prioridade máxima: {$count}.");
return self::SUCCESS;
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Domain\Interests\Enums;
enum InterestStatus: string
{
case Received = 'received';
case Submitted = 'submitted';
case InReview = 'in_review';
case Reviewed = 'reviewed';
case Qualification = 'qualification';
case Qualified = 'qualified';
case Contacted = 'contacted';
case ContactAccepted = 'contact_accepted';
case Referred = 'referred';
case DueDiligence = 'due_diligence';
case Negotiation = 'negotiation';
case Pending = 'pending';
case Refused = 'refused';
case Converted = 'converted';
case Cancelled = 'cancelled';
case Closed = 'closed';
}
@@ -0,0 +1,23 @@
<?php
namespace App\Domain\Opportunities\Enums;
enum OpportunityStatus: string
{
case Draft = 'draft';
case Submitted = 'submitted';
case Triage = 'triage';
case InformationPending = 'information_pending';
case InReview = 'in_review';
case Review = 'review';
case ApprovalL1 = 'approval_l1';
case ApprovalL2 = 'approval_l2';
case Approved = 'approved';
case Published = 'published';
case NeedsInformation = 'needs_information';
case Suspended = 'suspended';
case Rejected = 'rejected';
case Withdrawn = 'withdrawn';
case Cancelled = 'cancelled';
case Archived = 'archived';
}
@@ -2,13 +2,16 @@
namespace App\Http\Controllers\Api\V1;
use App\Domain\Interests\Enums\InterestStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreInterestRequestRequest;
use App\Jobs\NotifyInterestReceived;
use App\Models\ExistingBusiness;
use App\Models\InterestRequest;
use App\Models\Opportunity;
use App\Services\IdempotencyConflict;
use App\Services\InterestSubmission;
use Illuminate\Database\QueryException;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Queue;
@@ -33,8 +36,17 @@ class InterestRequestController extends Controller
throw ValidationException::withMessages(['opportunityReference' => 'A oportunidade seleccionada já não está disponível.']);
}
$business = ! empty($validated['existingBusinessId'])
? ExistingBusiness::published()->whereKey($validated['existingBusinessId'])->first()
: null;
if (! empty($validated['existingBusinessId']) && ! $business) {
throw ValidationException::withMessages(['existingBusinessId' => 'A oportunidade seleccionada já não está disponível.']);
}
$interest = InterestRequest::create([
'opportunity_id' => $opportunity?->id,
'existing_business_id' => $business?->id,
'modality' => $business ? $validated['modality'] : null,
'reference' => $this->generateReference(),
'full_name' => trim($validated['fullName']),
'email' => strtolower(trim($validated['email'])),
@@ -47,7 +59,7 @@ class InterestRequestController extends Controller
'message' => trim($validated['message']),
'privacy_consent' => (bool) $validated['privacyConsent'],
'consented_at' => now(),
'status' => 'received',
'status' => InterestStatus::Received->value,
'source_page' => Str::limit($request->headers->get('referer', ''), 255, ''),
'ip_address' => $request->ip(),
'user_agent' => $request->userAgent(),
@@ -78,6 +90,12 @@ class InterestRequestController extends Controller
} catch (IdempotencyConflict) {
return response()->json(['success' => false, 'message' => 'A chave de envio já foi usada com dados diferentes.'], 409);
} catch (ValidationException $exception) {
throw $exception;
} catch (QueryException $exception) {
if ($exception->getCode() === '23505') {
return response()->json(['success' => false, 'message' => 'Já existe uma manifestação activa para esta oportunidade e modalidade.'], 409);
}
throw $exception;
} catch (\Throwable $exception) {
Log::error('Erro ao registar manifestação de interesse', [
@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Models\InvestmentLegalSource;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class LegislationSourceController extends Controller
{
public function index(Request $request): JsonResponse
{
$filters = $request->validate([
'q' => ['nullable', 'string', 'max:100'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
]);
$today = today()->toDateString();
$query = InvestmentLegalSource::query()
->where('validation_status', 'validated')
->whereDate('valid_from', '<=', $today)
->where(fn ($query) => $query->whereNull('valid_until')->orWhereDate('valid_until', '>=', $today));
if (! empty($filters['q'])) {
$term = '%'.addcslashes($filters['q'], '%_\\').'%';
$query->where(fn ($query) => $query
->whereRaw('title ILIKE ?', [$term])
->orWhereRaw('diploma ILIKE ?', [$term])
->orWhereRaw('article ILIKE ?', [$term])
->orWhereRaw('competent_entity ILIKE ?', [$term]));
}
$page = $query->orderBy('title')->orderBy('id')->paginate($filters['per_page'] ?? 25)->withQueryString();
$data = collect($page->items())->map(fn (InvestmentLegalSource $source): array => [
'id' => $source->id,
'title' => $source->title,
'url' => $source->url,
'diploma' => $source->diploma,
'article' => $source->article,
'competent_entity' => $source->competent_entity,
'validation_status' => $source->validation_status,
'version_no' => (int) $source->version_no,
'valid_from' => $source->valid_from,
'valid_until' => $source->valid_until,
'validator_id' => $source->validator_id === null ? null : (int) $source->validator_id,
'validated_at' => $source->validated_at,
])->values();
$body = [
'data' => $data,
'meta' => [
'current_page' => $page->currentPage(),
'per_page' => $page->perPage(),
'total' => $page->total(),
],
];
$etag = 'W/"legal-sources-'.hash('sha256', json_encode([
'filters' => $filters,
'data' => $data,
'total' => $page->total(),
], JSON_THROW_ON_ERROR)).'"';
return response()->json($body)
->header('ETag', $etag)
->header('Cache-Control', 'public, max-age=300');
}
}
@@ -0,0 +1,187 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class ReferenceCatalogController extends Controller
{
private const CATALOGS = ['authorities', 'jurisdictions', 'economic-activities'];
public function index(Request $request, string $catalog): JsonResponse
{
abort_unless(in_array($catalog, self::CATALOGS, true), 404);
$rules = [
'q' => ['nullable', 'string', 'max:100'],
'version' => ['nullable', 'integer', 'min:1'],
'page' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
'parent_id' => ['nullable', 'uuid'],
'jurisdiction_id' => ['nullable', 'uuid'],
];
if ($catalog === 'jurisdictions') {
$rules['type'] = ['required', 'in:country,province,district,admin_post,locality'];
$rules['parent_id'][] = 'prohibited_if:type,country';
$rules['parent_id'][] = 'required_if:type,district,admin_post,locality';
}
if ($catalog === 'economic-activities') {
$rules['sector_id'] = ['nullable', 'integer', 'exists:sectors,id'];
}
$data = $request->validate($rules);
$versionQuery = DB::table('reference_catalog_versions')->where('catalog_code', $catalog)->where('locale', 'pt-MZ');
if (isset($data['version'])) {
$versionQuery->where('version_no', $data['version'])->whereIn('status', ['published', 'retired']);
} else {
$versionQuery->where('status', 'published')
->where(fn (Builder $query) => $query->whereNull('valid_from')->orWhere('valid_from', '<=', today()->toDateString()))
->where(fn (Builder $query) => $query->whereNull('valid_to')->orWhere('valid_to', '>', today()->toDateString()));
}
$version = $versionQuery->orderByDesc('version_no')->first();
if (isset($data['version']) && ! $version) {
abort(404, 'Versão publicada do catálogo não encontrada.');
}
if (! $version) {
return response()->json([
'data' => [],
'links' => ['next' => null, 'previous' => null],
'next_page_url' => null,
'prev_page_url' => null,
'current_page' => 1,
'per_page' => $data['per_page'] ?? 20,
'total' => 0,
'last_page' => 1,
'meta' => ['catalog' => $catalog, 'version' => null, 'locale' => 'pt-MZ', 'status' => 'not_published',
'current_page' => 1, 'per_page' => $data['per_page'] ?? 20, 'total' => 0, 'last_page' => 1],
])->header('Cache-Control', 'public, max-age=60');
}
$query = match ($catalog) {
'authorities' => $this->authorities((string) $version->id, $data),
'jurisdictions' => $this->jurisdictions($data),
'economic-activities' => $this->economicActivities((string) $version->id, $data),
};
$page = $query->orderBy('code')->orderBy('id')->paginate($data['per_page'] ?? 20)->withQueryString();
$items = collect($page->items())->map(fn (object $row): array => $this->present($catalog, $row))->values();
return response()->json([
'data' => $items,
'links' => ['next' => $page->nextPageUrl(), 'previous' => $page->previousPageUrl()],
'next_page_url' => $page->nextPageUrl(),
'prev_page_url' => $page->previousPageUrl(),
'current_page' => $page->currentPage(),
'per_page' => $page->perPage(),
'total' => $page->total(),
'last_page' => $page->lastPage(),
'meta' => ['catalog' => $catalog, 'version' => (int) $version->version_no, 'locale' => $version->locale,
'source_version' => $version->source_version, 'valid_from' => $version->valid_from, 'valid_to' => $version->valid_to,
'current_page' => $page->currentPage(), 'per_page' => $page->perPage(), 'total' => $page->total(), 'last_page' => $page->lastPage()],
])->header('Cache-Control', 'public, max-age=300')
->header('ETag', 'W/"catalog-'.substr(hash('sha256', $catalog.'|'.$version->version_no.'|'.json_encode($data, JSON_THROW_ON_ERROR)), 0, 32).'"');
}
/** @param array<string,mixed> $data */
private function authorities(string $versionId, array $data): Builder
{
$query = DB::table('authorities as a')->join('organizations as o', 'o.id', '=', 'a.organization_id')
->leftJoin('geo_locations as g', 'g.id', '=', 'a.jurisdiction_id')
->where('a.catalog_version_id', $versionId)->where('a.status', 'published')
->where('o.data_classification', 'public')->whereNull('o.deleted_at')
->where('a.valid_from', '<=', today()->toDateString())
->where(fn (Builder $builder) => $builder->whereNull('a.valid_to')->orWhere('a.valid_to', '>', today()->toDateString()))
->select(['a.id', 'a.code', 'a.authority_type', 'a.label_pt_mz as name', 'a.organization_id', 'a.jurisdiction_id',
'g.name as jurisdiction_name', 'a.version_no']);
if (! empty($data['jurisdiction_id'])) {
$query->where('a.jurisdiction_id', $data['jurisdiction_id']);
}
if (! empty($data['q'])) {
$this->search($query, $data['q'], ['a.code', 'a.label_pt_mz', 'o.name']);
}
return $query;
}
/** @param array<string,mixed> $data */
private function jurisdictions(array $data): Builder
{
$query = DB::table('geo_locations')->where('source_system', 'cartografia-workbook')
->where('data_classification', 'public')->whereNull('deleted_at')->where('location_type', $data['type'])
->select(['id', 'code', 'name', 'parent_id', 'location_type', 'source_code', 'version_no']);
$parentType = ['province' => 'country', 'district' => 'province', 'admin_post' => 'district', 'locality' => 'admin_post'][$data['type']] ?? null;
if ($data['type'] !== 'country' && ! empty($data['parent_id'])) {
$validParent = DB::table('geo_locations')->where('id', $data['parent_id'])->where('location_type', $parentType)
->where('source_system', 'cartografia-workbook')->where('data_classification', 'public')->whereNull('deleted_at')->exists();
if (! $validParent) {
throw ValidationException::withMessages(['parent_id' => 'Seleccione um território pai activo do nível anterior.']);
}
$query->where('parent_id', $data['parent_id']);
} elseif (in_array($data['type'], ['district', 'admin_post', 'locality'], true)) {
throw ValidationException::withMessages(['parent_id' => 'Seleccione primeiro o território pai.']);
}
if (! empty($data['q'])) {
$this->search($query, $data['q'], ['code', 'name', 'source_code']);
}
return $query;
}
/** @param array<string,mixed> $data */
private function economicActivities(string $versionId, array $data): Builder
{
$query = DB::table('economic_activities')->where('catalog_version_id', $versionId)->where('status', 'published')
->where('valid_from', '<=', today()->toDateString())
->where(fn (Builder $builder) => $builder->whereNull('valid_to')->orWhere('valid_to', '>', today()->toDateString()))
->select(['id', 'code', 'label_pt_mz as name', 'parent_id', 'sector_id', 'version_no']);
if (array_key_exists('parent_id', $data) && $data['parent_id'] !== null) {
$query->where('parent_id', $data['parent_id']);
} else {
$query->whereNull('parent_id');
}
if (! empty($data['sector_id'])) {
$query->where('sector_id', $data['sector_id']);
}
if (! empty($data['q'])) {
$this->search($query, $data['q'], ['code', 'label_pt_mz']);
}
return $query;
}
/** @param list<string> $columns */
private function search(Builder $query, string $search, array $columns): void
{
$term = '%'.addcslashes($search, '%_\\').'%';
$query->where(function (Builder $nested) use ($columns, $term): void {
foreach ($columns as $index => $column) {
$method = $index === 0 ? 'whereRaw' : 'orWhereRaw';
$nested->{$method}($column.' ILIKE ?', [$term]);
}
});
}
/** @return array<string,mixed> */
private function present(string $catalog, object $row): array
{
return match ($catalog) {
'jurisdictions' => ['id' => $row->id, 'value' => $row->id, 'code' => $row->code, 'name' => $row->name,
'label' => $row->name, 'locale' => 'pt-MZ', 'parent_id' => $row->parent_id, 'type' => $row->location_type,
'source_code' => $row->source_code, 'version_no' => $row->version_no],
'economic-activities' => ['id' => $row->id, 'value' => $row->code, 'code' => $row->code, 'name' => $row->name,
'label' => $row->name, 'locale' => 'pt-MZ', 'parent_id' => $row->parent_id, 'sector_id' => $row->sector_id,
'version_no' => $row->version_no],
'authorities' => ['id' => $row->id, 'value' => $row->code, 'code' => $row->code, 'name' => $row->name,
'label' => $row->name, 'locale' => 'pt-MZ', 'authority_type' => $row->authority_type,
'organization_id' => $row->organization_id, 'jurisdiction_id' => $row->jurisdiction_id,
'jurisdiction_name' => $row->jurisdiction_name, 'version_no' => $row->version_no],
};
}
}
@@ -0,0 +1,119 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreSimulationRequest;
use App\Http\Requests\UpdateSimulationScenarioRequest;
use App\Models\QuestionnaireVersion;
use App\Models\SimulationJob;
use App\Policies\SimulationPolicy;
use App\Services\SimulationExecution;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class SimulationController extends Controller
{
public function questionnaire(string $code): JsonResponse
{
abort_unless(preg_match('/^[a-z0-9][a-z0-9-]{0,79}$/', $code) === 1, 404);
$version = QuestionnaireVersion::query()->where('code', $code)->where('locale', 'pt-MZ')->where('status', 'published')
->where(fn ($query) => $query->whereNull('valid_from')->orWhereDate('valid_from', '<=', today()))
->where(fn ($query) => $query->whereNull('valid_to')->orWhereDate('valid_to', '>', today()))
->orderByDesc('version_no')->firstOrFail();
$etag = '"questionnaire-'.$version->id.'-v'.$version->version_no.'"';
return response()->json(['data' => $version])->header('ETag', $etag)->header('Cache-Control', 'private, max-age=60');
}
public function store(StoreSimulationRequest $request, SimulationExecution $execution): JsonResponse
{
$key = $request->header('Idempotency-Key');
abort_unless(is_string($key) && strlen($key) >= 16 && strlen($key) <= 128, 428, 'Idempotency-Key é obrigatório.');
$actorId = $request->user()?->id;
$token = $actorId === null ? $request->header('X-Simulation-Token') : null;
abort_if($actorId === null && (! is_string($token) || strlen($token) < 32 || strlen($token) > 256), 422, 'X-Simulation-Token opaco gerado pelo cliente é obrigatório para execução anónima.');
$validated = $request->validated();
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0'), 'organization_id' => $validated['organization_id'] ?? null];
$organizationId = $data['organization_id'] ?? null;
$execution->authorizeOrganization($actorId, $organizationId);
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
$simulation = $result['simulation'];
$body = ['data' => $simulation, 'replayed' => $result['replayed']];
if ($actorId === null) {
$body['access_token'] = $token;
}
return response()->json($body, $result['replayed'] ? 200 : 202)
->header('ETag', $execution->etag($simulation))
->header('Location', url('/api/v1/simulations/'.$simulation->id));
}
public function show(Request $request, string $id, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->view($request->user(), $simulation), 404);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
return response()->json(['data' => $simulation])->header('ETag', $execution->etag($simulation));
}
public function updateScenario(UpdateSimulationScenarioRequest $request, string $id, string $scenarioId, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id) && Str::isUuid($scenarioId), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->update($request->user(), $simulation), 403);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
$validated = $request->validated();
$data = ['label' => (string) $validated['label'], 'base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0')];
$updated = $execution->updateScenario($id, $scenarioId, $data, $request);
return response()->json(['data' => $updated], 202)->header('ETag', $execution->etag($updated));
}
public function report(Request $request, string $id, string $reportId, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id) && Str::isUuid($reportId), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->view($request->user(), $simulation), 404);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
$report = $simulation->reports->firstWhere('id', $reportId);
abort_if($report === null, 404);
return response()->json(['data' => $report]);
}
public function jobs(Request $request, string $id, SimulationExecution $execution): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$simulation = $execution->find($id, $request);
if ($request->user()) {
abort_unless(app(SimulationPolicy::class)->view($request->user(), $simulation), 404);
} else {
abort_unless($simulation->anonymous_token_hash !== null && hash_equals($simulation->anonymous_token_hash, (string) $execution->tokenHash($request)), 404);
}
$jobs = DB::transaction(function () use ($request, $id, $execution) {
$execution->setContext($request->user()?->id, $execution->tokenHash($request));
return SimulationJob::query()->where('simulation_id', $id)
->orderBy('created_at')->orderBy('id')->get();
});
return response()->json(['data' => $jobs]);
}
}
@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreSimulationFormDraftRequest;
use App\Models\SimulationFormDraft;
use App\Services\SimulationFormDrafts;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class SimulationFormDraftController extends Controller
{
public function store(StoreSimulationFormDraftRequest $request, SimulationFormDrafts $drafts): JsonResponse
{
$key = $request->header('Idempotency-Key');
abort_unless(is_string($key) && strlen($key) >= 16 && strlen($key) <= 128, 428, 'Idempotency-Key é obrigatório.');
$result = $drafts->create($request->validated(), $request, $key);
$draft = $result['draft'];
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no, 'replayed' => $result['replayed']]], $result['replayed'] ? 200 : 201)
->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function current(Request $request, SimulationFormDrafts $drafts): JsonResponse
{
$draft = $drafts->current($request);
if (! $draft) {
return response()->json(['data' => null], 200)->header('Cache-Control', 'no-store, private');
}
return response()->json(['data' => $drafts->present($draft)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function show(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$draft = $drafts->find($id, $request);
return response()->json(['data' => $drafts->present($draft)])->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function update(StoreSimulationFormDraftRequest $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$draft = $drafts->update($id, $request->validated(), $request);
return response()->json(['data' => ['id' => $draft->id, 'version_no' => $draft->version_no]])
->header('ETag', $drafts->etag($draft))->header('Cache-Control', 'no-store, private');
}
public function destroy(Request $request, string $id, SimulationFormDrafts $drafts): JsonResponse
{
abort_unless(Str::isUuid($id), 404);
$drafts->delete($id, $request);
return response()->json([], 204)->header('Cache-Control', 'no-store, private');
}
}
@@ -28,7 +28,9 @@ class ExistingBusinessController extends Controller
$query = app(BusinessRepository::class)->query();
if ($management) {
Gate::authorize('reviewQueue', ExistingBusiness::class);
$query->where('owner_user_id', '<>', $request->user()->id)->whereIn('status', ['submitted', 'in_review', 'needs_information']);
$query->where('owner_user_id', '<>', $request->user()->id)->whereIn('status', [
'submitted', 'triage', 'in_review', 'needs_information', 'approval_l1', 'approval_l2', 'approved',
]);
} else {
$query->where('owner_user_id', $request->user()->id);
}
@@ -45,6 +47,10 @@ class ExistingBusinessController extends Controller
app(CompatibilityShadow::class)->schedule($id);
$props = ['sectors' => Sector::where('is_active', true)->orderBy('name')->get(['id', 'name']), 'formSchema' => BusinessFormSchema::fields(), 'record' => $this->record($business), 'canEdit' => $request->user()->can('update', $business),
'owner' => $request->user()->can('owns', $business), 'canReview' => $request->user()->can('review', $business),
'canTriage' => $request->user()->can('triage', $business),
'canApproveLevelOne' => $request->user()->can('approveLevelOne', $business),
'canApproveLevelTwo' => $request->user()->can('approveLevelTwo', $business),
'canPublish' => $request->user()->can('publish', $business),
'modalities' => ExistingBusinessDrafts::MODALITIES,
'objectKinds' => ExistingBusinessObjects::KINDS,
'verificationChecks' => ExistingBusinessVerification::CHECKS,
@@ -90,6 +96,10 @@ class ExistingBusinessController extends Controller
/** @return array<string,mixed> */
private function record(ExistingBusiness $business): array
{
return [...$business->only(['id', 'code', 'modality', 'status', 'classification', 'version_no', 'sector_id']), 'payload' => $business->payload];
return [...$business->only([
'id', 'code', 'modality', 'status', 'classification', 'version_no', 'sector_id', 'public_title', 'public_slug',
'activity_code', 'stage', 'objective', 'currency', 'amount_min', 'amount_max', 'available_percentage',
'funds_use', 'submitted_at', 'published_at', 'expires_at', 'next_action', 'next_action_due_at', 'assigned_to',
]), 'payload' => $business->payload];
}
}
@@ -27,7 +27,8 @@ class InvestmentLegalSourceController extends Controller
$source->refresh();
$this->audit($source, $request->user()->id, 'created');
return response()->json($source->fresh(), 201);
return response()->json($source->fresh(), 201)
->header('Location', url('/api/v2/investment/admin/legal-sources/'.$source->id));
});
}
@@ -6,12 +6,16 @@ use App\Models\InterestRequest;
use App\Models\InvestmentSubmission;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Models\User;
use App\Services\InterestWorkflow;
use App\Services\InvestmentGeography;
use App\Services\InvestmentWorkspace;
use App\Services\PortalAudit;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
@@ -22,7 +26,7 @@ use Inertia\Response;
class PortalManagementController extends Controller
{
private const STATES = ['received', 'in_review', 'contacted', 'closed'];
private const STATES = ['received', 'submitted', 'in_review', 'reviewed', 'qualification', 'qualified', 'contacted', 'contact_accepted', 'referred', 'due_diligence', 'negotiation', 'pending', 'refused', 'converted', 'cancelled', 'closed'];
public function history(): Response
{
@@ -50,9 +54,10 @@ class PortalManagementController extends Controller
$filters = $request->validate([
'q' => ['nullable', 'string', 'max:150'],
'status' => ['nullable', Rule::in(self::STATES)],
'follow_up' => ['nullable', Rule::in(['overdue', 'upcoming', 'unassigned'])],
]);
$query = InterestRequest::query()->select([
'id', 'reference', 'full_name', 'email', 'sector_interest', 'status', 'created_at',
$query = InterestRequest::query()->with('assignee:id,name')->select([
'id', 'reference', 'full_name', 'email', 'sector_interest', 'existing_business_id', 'modality', 'status', 'assigned_to', 'next_action', 'next_action_due_at', 'created_at',
]);
if ($term = $filters['q'] ?? null) {
$query->where(fn ($q) => $q->whereLike('reference', "%{$term}%")
@@ -61,6 +66,15 @@ class PortalManagementController extends Controller
if ($status = $filters['status'] ?? null) {
$query->where('status', $status);
}
if (($filters['follow_up'] ?? null) === 'overdue') {
$query->whereNotNull('next_action_due_at')->where('next_action_due_at', '<', now())
->whereNotIn('status', ['refused', 'converted', 'cancelled', 'closed']);
} elseif (($filters['follow_up'] ?? null) === 'upcoming') {
$query->whereNotNull('next_action_due_at')->where('next_action_due_at', '>=', now())
->whereNotIn('status', ['refused', 'converted', 'cancelled', 'closed']);
} elseif (($filters['follow_up'] ?? null) === 'unassigned') {
$query->whereNull('assigned_to')->whereNotIn('status', ['refused', 'converted', 'cancelled', 'closed']);
}
return Inertia::render('management/interests', [
'requests' => $query->orderByDesc('id')->paginate(20)->withQueryString(),
@@ -70,31 +84,94 @@ class PortalManagementController extends Controller
public function interest(InterestRequest $interest): Response
{
$history = DB::table('business_workflow_transitions')->where('interest_request_id', $interest->id)
->orderByDesc('created_at')->limit(50)->get(['from_status', 'to_status', 'actor_id', 'reason', 'created_at'])
->map(function ($row) {
$row->note = $row->reason ? Crypt::decryptString($row->reason) : null;
unset($row->reason);
return $row;
});
$consents = DB::table('consents')->where('interest_request_id', $interest->id)
->orderBy('created_at')->get(['purpose', 'policy_version', 'status', 'granted_at', 'withdrawn_at']);
return Inertia::render('management/interest', [
'interest' => $interest->only([
'id', 'reference', 'full_name', 'email', 'phone', 'investor_type', 'country',
'sector_interest', 'opportunity_reference', 'investment_range', 'message',
'privacy_consent', 'consented_at', 'status', 'created_at', 'reviewed_at', 'reviewed_by', 'metadata',
'existing_business_id', 'modality', 'assigned_to', 'next_action', 'next_action_due_at',
]),
'history' => $history,
'consents' => $consents,
'assignees' => $this->interestAssignees()->get(['id', 'name']),
]);
}
public function review(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
public function review(Request $request, InterestRequest $interest, InterestWorkflow $workflow): RedirectResponse|JsonResponse
{
$validated = $request->validate(['status' => ['required', Rule::in(self::STATES)]]);
DB::transaction(function () use ($request, $interest, $validated) {
$locked = InterestRequest::whereKey($interest->id)->lockForUpdate()->firstOrFail();
$changed = $locked->status !== $validated['status'];
PortalAudit::save($locked, [
...$validated, 'reviewed_at' => now(), 'reviewed_by' => $request->user()->id,
], $request->user()->id);
$submission = InvestmentSubmission::where('interest_request_id', $interest->id)->first();
if ($changed && $submission) {
app(InvestmentWorkspace::class)->notify($submission->user_id, $submission, 'status', (string) Str::uuid(), ['status' => $validated['status']]);
$validated = $request->validate([
'status' => ['required', Rule::in(self::STATES)],
'note' => ['nullable', 'string', 'min:10', 'max:2000'],
'next_action' => ['nullable', 'required_with:next_action_due_at', 'string', 'max:180'],
'next_action_due_at' => ['nullable', 'required_with:next_action', 'date', 'after_or_equal:today'],
'assigned_to' => ['nullable', 'integer', Rule::in($this->interestAssignees()->pluck('id'))],
]);
$before = $interest->status;
$updated = $workflow->transition($request->user(), $interest, $validated['status'], $validated['note'] ?? null,
$validated['next_action'] ?? null, $validated['next_action_due_at'] ?? null, isset($validated['assigned_to']) ? (int) $validated['assigned_to'] : null,
$request->exists('assigned_to'), $request->exists('next_action') || $request->exists('next_action_due_at'));
PortalAudit::save($updated, ['status' => $updated->status, 'reviewed_at' => $updated->reviewed_at, 'reviewed_by' => $updated->reviewed_by], $request->user()->id);
$submission = InvestmentSubmission::where('interest_request_id', $interest->id)->first();
if ($before !== $updated->status && $submission) {
app(InvestmentWorkspace::class)->notify($submission->user_id, $submission, 'status', (string) Str::uuid(), ['status' => $updated->status]);
}
return $request->expectsJson() ? response()->json(['reference' => $updated->reference, 'status' => $updated->status]) : back()->with('success', 'Estado do pedido actualizado.');
}
public function withdrawProponentSharingConsent(Request $request, InterestRequest $interest): RedirectResponse|JsonResponse
{
$data = $request->validate(['reason' => ['required', 'string', 'min:10', 'max:2000']]);
DB::transaction(function () use ($request, $interest, $data) {
$consent = DB::table('consents')->where('interest_request_id', $interest->id)
->where('purpose', 'share_contact_with_proponent')->where('status', 'granted')->lockForUpdate()->first();
if (! $consent) {
return;
}
DB::table('consents')->where('id', $consent->id)->update(['status' => 'withdrawn', 'withdrawn_at' => now(), 'updated_at' => now()]);
$correlationId = (string) Str::uuid();
DB::table('business_workflow_transitions')->insert([
'id' => (string) Str::uuid(), 'interest_request_id' => $interest->id,
'from_status' => $interest->status, 'to_status' => $interest->status,
'actor_id' => $request->user()->id, 'reason' => Crypt::encryptString($data['reason']),
'correlation_id' => $correlationId,
'metadata' => json_encode(['source' => 'management-interest', 'action' => 'consent_withdrawn',
'purpose' => 'share_contact_with_proponent'], JSON_THROW_ON_ERROR),
'created_at' => now(),
]);
DB::table('outbox_events')->insertOrIgnore([
'source_system' => 'portal-intake', 'external_id' => 'InterestConsentWithdrawn:'.$consent->id,
'aggregate_type' => 'interest_consent', 'aggregate_id' => $consent->id,
'event_type' => 'InterestConsentWithdrawn', 'data_classification' => 'confidential',
'correlation_id' => $correlationId, 'status' => 'pending',
'payload' => json_encode(['schema_version' => 1, 'consent_id' => $consent->id,
'interest_id' => $interest->integration_id, 'purpose' => 'share_contact_with_proponent'], JSON_THROW_ON_ERROR),
]);
});
return $request->expectsJson() ? response()->json(['reference' => $interest->reference, 'status' => $validated['status']]) : back()->with('success', 'Estado do pedido actualizado.');
return $request->expectsJson() ? response()->json(['reference' => $interest->reference, 'sharing_consent' => 'withdrawn'])
: back()->with('success', 'Consentimento de partilha revogado.');
}
private function interestAssignees(): Builder
{
$emails = array_unique(array_merge(config('portal.managers', []), ...array_map(
fn (string $designation) => config('existing-business.'.$designation, []),
['triagers', 'reviewers', 'approvers_l1', 'approvers_l2', 'publishers'],
)));
return User::query()->whereNotNull('email_verified_at')->whereIn(DB::raw('lower(email)'), $emails)->orderBy('name');
}
public function catalog(Request $request, string $resource): Response
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\PublicBusinessOpportunityResource;
use App\Models\ExistingBusiness;
use App\Services\ExistingBusinessDrafts;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Inertia\Response;
class PublicBusinessOpportunityController extends Controller
{
public function index(Request $request): Response|JsonResponse
{
$filters = $request->validate([
'q' => ['nullable', 'string', 'max:120'],
'sector' => ['nullable', 'integer', 'min:1'],
'per_page' => ['nullable', 'integer', 'min:1', 'max:50'],
]);
$query = $this->publishedQuery();
if (! empty($filters['q'])) {
$term = trim($filters['q']);
$query->where(fn (Builder $builder) => $builder->whereLike('existing_businesses.public_title', '%'.$term.'%')
->orWhereLike('existing_businesses.activity_code', '%'.$term.'%')
->orWhereLike('existing_businesses.objective', '%'.$term.'%'));
}
if (! empty($filters['sector'])) {
$query->where('existing_businesses.sector_id', $filters['sector']);
}
$results = PublicBusinessOpportunityResource::collection($query->paginate($filters['per_page'] ?? 12)->withQueryString());
if ($request->expectsJson()) {
return $results->response();
}
return Inertia::render('investment/business-marketplace', ['results' => $results, 'filters' => $filters]);
}
public function show(Request $request, string $slug): Response|JsonResponse
{
$record = $this->publishedQuery()->where('existing_businesses.public_slug', $slug)->firstOrFail();
$resource = new PublicBusinessOpportunityResource($record);
if ($request->expectsJson()) {
return $resource->response();
}
return Inertia::render('investment/business-opportunity', ['record' => $resource]);
}
public function interest(Request $request, string $slug): Response
{
$record = $this->publishedQuery()->where('existing_businesses.public_slug', $slug)->firstOrFail();
return Inertia::render('investment/business-interest', [
'record' => (new PublicBusinessOpportunityResource($record))->resolve($request),
'modalities' => ExistingBusinessDrafts::MODALITIES,
]);
}
/** @return Builder<ExistingBusiness> */
private function publishedQuery(): Builder
{
$publicLocations = DB::table('existing_business_locations as locations')
->leftJoin('geo_locations as province', 'province.id', '=', 'locations.province_id')
->leftJoin('geo_locations as district', 'district.id', '=', 'locations.district_id')
->leftJoin('geo_locations as locality', 'locality.id', '=', 'locations.locality_id')
->where('locations.is_public', true)->where('locations.is_primary', true)
->whereNull('province.deleted_at')->whereNull('district.deleted_at')->whereNull('locality.deleted_at')
->select('locations.existing_business_id', 'province.name as province')
->selectRaw("CASE WHEN locations.precision_level IN ('district','admin_post','locality','exact') THEN district.name ELSE NULL END AS district")
->selectRaw('NULL::text AS locality');
return ExistingBusiness::query()->published()->with('sector:id,name,slug')
->leftJoinSub($publicLocations, 'public_location', 'public_location.existing_business_id', '=', 'existing_businesses.id')
->select(['existing_businesses.id', 'existing_businesses.public_slug', 'existing_businesses.public_title',
'existing_businesses.activity_code', 'existing_businesses.stage', 'existing_businesses.objective',
'existing_businesses.currency', 'existing_businesses.amount_min', 'existing_businesses.amount_max',
'existing_businesses.available_percentage', 'existing_businesses.published_at', 'existing_businesses.sector_id',
'public_location.province', 'public_location.district', 'public_location.locality']);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class CorrelationId
{
/** @param Closure(Request): Response $next */
public function handle(Request $request, Closure $next): Response
{
$provided = $request->header('X-Correlation-ID');
$correlationId = is_string($provided) && Str::isUuid($provided)
? $provided
: (string) Str::uuid();
if ($provided !== null && ! Str::isUuid($provided)) {
return response()->json([
'message' => 'O identificador de correlação deve ser um UUID válido.',
'code' => 'invalid_correlation_id',
'correlation_id' => $correlationId,
], 422)->header('X-Correlation-ID', $correlationId);
}
$request->attributes->set('correlation_id', $correlationId);
$response = $next($request);
$response->headers->set('X-Correlation-ID', $correlationId);
return $response;
}
}
@@ -2,10 +2,12 @@
namespace App\Http\Requests;
use App\Services\ExistingBusinessDrafts;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Validator;
class StoreInterestRequestRequest extends FormRequest
{
@@ -36,12 +38,28 @@ class StoreInterestRequestRequest extends FormRequest
->where('status', 'published')->whereNotNull('published_at')
->where('published_at', '<=', now())->whereNull('deleted_at')),
])],
'existingBusinessId' => ['nullable', 'uuid', ...($knownKey ? [] : [
Rule::exists('existing_businesses', 'id')->where(fn ($query) => $query
->where('status', 'published')->whereNotNull('published_at')->where('published_at', '<=', now())->whereNull('deleted_at')),
])],
'modality' => ['nullable', 'required_with:existingBusinessId', 'string', 'max:50', Rule::in(ExistingBusinessDrafts::MODALITIES)],
'investmentRange' => ['nullable', 'string', 'max:80'],
'message' => ['required', 'string', 'min:20', 'max:2000'],
'privacyConsent' => ['required', 'accepted'],
'shareWithProponent' => ['sometimes', 'boolean'],
];
}
/** @return list<callable(Validator): void> */
public function after(): array
{
return [function (Validator $validator): void {
if ($this->filled('existingBusinessId') && $this->filled('opportunityReference')) {
$validator->errors()->add('existingBusinessId', 'Seleccione apenas uma oportunidade.');
}
}];
}
public function messages(): array
{
return [
@@ -0,0 +1,195 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
use Illuminate\Support\Facades\DB;
class StoreSimulationFormDraftRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, list<mixed>> */
public function rules(): array
{
$personal = [
'person_type', 'investor_origin', 'tax_residency_country', 'entity_stage', 'legal_form', 'nuit',
'investor_profile_id', 'profile_confirmation', 'legal_name', 'commercial_name', 'registration_number',
'representative_name', 'representative_capacity', 'email', 'phone', 'address', 'contact_country', 'selection_labels',
'contact_province', 'contact_district', 'preferred_channel',
];
$economic = [
'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'selection_labels', 'project_type',
'project_type_other', 'sector_id', 'activity_id', 'products_services', 'maturity_stage', 'expected_start_date',
'project_duration_years', 'existing_project_id', 'province_id', 'district_id', 'admin_post_id', 'locality_id',
'site_known', 'latitude', 'longitude', 'area_required', 'area_unit', 'special_zone_id', 'land_status',
'infrastructure_needs', 'currency', 'total_investment', 'own_capital', 'domestic_debt', 'foreign_debt',
'grants', 'secured_amount', 'sought_amount', 'annual_revenue', 'annual_operating_costs', 'expected_profit',
'financing_term_months', 'funds_origin_countries', 'direct_jobs', 'indirect_jobs', 'national_jobs',
'foreign_jobs', 'skills_needed', 'local_content_percentage', 'exports_value', 'import_substitution_value',
'environmental_category', 'community_benefits', 'tax_calculation', 'base', 'rate', 'reduction',
'sales_taxable', 'sales_exempt', 'sales_zero_rate', 'exports', 'deductible_costs', 'non_deductible_costs',
'carried_losses', 'payroll', 'outbound_payments', 'imports', 'claimed_benefits', 'reference_date',
'offer_or_demand', 'opportunity_type', 'opportunity_type_other', 'opportunity_description',
'opportunity_value', 'opportunity_currency', 'opportunity_conditions', 'opportunity_deadline',
'required_documents', 'confidentiality', 'publish_requested', 'terms_accepted', 'truth_declaration', 'reuse_simulation',
'contact_authorisation', 'publication_authorisation', 'public_fields_confirmation', 'validation_acknowledged',
'final_declaration_name',
];
$rules = [
'personal_snapshot' => ['required', 'array:'.implode(',', $personal)],
'economic_snapshot' => ['required', 'array:'.implode(',', $economic)],
'economic_snapshot.purpose' => ['required', 'in:simulate_only,own_investment,seek_investor,seek_partner,present_opportunity,request_facilitation'],
'economic_snapshot.manifestation_type' => ['nullable', 'in:investment,business'],
'economic_snapshot.preferred_language' => ['nullable', 'in:pt-MZ,en'],
'economic_snapshot.project_type' => ['nullable', 'in:new,expansion,acquisition,partnership,concession,supplier,buyer,other'],
'economic_snapshot.maturity_stage' => ['nullable', 'in:idea,concept,pre_feasibility,feasibility,financing,implementation,operation'],
'economic_snapshot.land_status' => ['nullable', 'in:owned,duat,leased,requested,unknown'],
'economic_snapshot.offer_or_demand' => ['nullable', 'in:offer,demand,both'],
'economic_snapshot.opportunity_type' => ['nullable', 'in:capital,partner,buyer,supplier,land,technology,licence,services,other'],
'economic_snapshot.confidentiality' => ['nullable', 'in:public,registered_users,restricted,confidential'],
'economic_snapshot.site_known' => ['nullable', 'in:yes,no'],
'economic_snapshot.tax_calculation' => ['nullable', 'in:yes,no'],
'economic_snapshot.publish_requested' => ['nullable', 'in:yes,no'],
'economic_snapshot.terms_accepted' => ['nullable', 'in:yes,no'],
'economic_snapshot.truth_declaration' => ['nullable', 'in:yes,no'],
'economic_snapshot.reuse_simulation' => ['nullable', 'in:yes,no'],
'economic_snapshot.contact_authorisation' => ['nullable', 'in:yes,no'],
'economic_snapshot.publication_authorisation' => ['nullable', 'in:yes,no'],
'economic_snapshot.public_fields_confirmation' => ['nullable', 'in:yes,no'],
'economic_snapshot.validation_acknowledged' => ['nullable', 'in:yes,no'],
'economic_snapshot.currency' => ['nullable', 'string', 'regex:/^[A-Z]{3}$/'],
'economic_snapshot.opportunity_currency' => ['nullable', 'string', 'regex:/^[A-Z]{3}$/'],
'economic_snapshot.rate' => ['nullable', 'numeric', 'between:0,100'],
'economic_snapshot.reduction' => ['nullable', 'numeric', 'between:0,100'],
'economic_snapshot.local_content_percentage' => ['nullable', 'numeric', 'between:0,100'],
'economic_snapshot.latitude' => ['nullable', 'numeric', 'between:-90,90'],
'economic_snapshot.longitude' => ['nullable', 'numeric', 'between:-180,180'],
'economic_snapshot.expected_start_date' => ['nullable', 'date'],
'economic_snapshot.opportunity_deadline' => ['nullable', 'date'],
'economic_snapshot.reference_date' => ['nullable', 'date'],
'economic_snapshot.*' => ['nullable'],
];
foreach (['base', 'area_required', 'total_investment', 'own_capital', 'domestic_debt', 'foreign_debt', 'grants',
'secured_amount', 'sought_amount', 'annual_revenue', 'annual_operating_costs', 'expected_profit', 'exports_value',
'import_substitution_value', 'sales_taxable', 'sales_exempt', 'sales_zero_rate', 'exports', 'deductible_costs',
'non_deductible_costs', 'carried_losses', 'opportunity_value'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'numeric', 'min:0'];
}
foreach (['direct_jobs', 'indirect_jobs', 'national_jobs', 'foreign_jobs', 'financing_term_months', 'project_duration_years'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'integer', 'min:0'];
}
foreach (['infrastructure_needs', 'funds_origin_countries', 'required_documents'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'array', 'max:100'];
$rules['economic_snapshot.'.$field.'.*'] = ['nullable', 'string', 'max:3000'];
}
$rules['economic_snapshot.infrastructure_needs.*'] = ['nullable', 'in:energia,água,estrada,porto,ferrovia,telecomunicações,armazenamento'];
$rules['economic_snapshot.required_documents.*'] = ['nullable', 'in:identity_document,company_registration,nuit_certificate,company_profile,business_plan,feasibility_study,proof_of_funds,special_zone_certificate,licence,site_map'];
foreach (['skills_needed', 'payroll', 'outbound_payments', 'imports', 'claimed_benefits', 'opportunity_conditions'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'string', 'max:10000'];
}
$rules['economic_snapshot.selection_labels'] = ['nullable', 'array'];
foreach (['project_summary', 'opportunity_description', 'products_services', 'community_benefits'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'string', 'max:10000'];
}
$rules['personal_snapshot.email'] = ['nullable', 'email:rfc', 'max:254'];
$rules['personal_snapshot.nuit'] = ['nullable', 'string', 'max:32'];
$rules['personal_snapshot.investor_profile_id'] = ['nullable', 'uuid'];
$rules['personal_snapshot.profile_confirmation'] = ['nullable', 'boolean'];
$rules['personal_snapshot.selection_labels'] = ['nullable', 'array'];
$rules['personal_snapshot.person_type'] = ['nullable', 'in:singular,collective,sole_trader,consortium'];
$rules['personal_snapshot.investor_origin'] = ['nullable', 'in:national,foreign,mixed'];
$rules['personal_snapshot.entity_stage'] = ['nullable', 'in:existing,being_formed'];
$rules['economic_snapshot.sector_id'] = ['nullable', 'integer', 'exists:sectors,id'];
$rules['economic_snapshot.activity_id'] = ['nullable', 'uuid', 'exists:economic_activities,id'];
foreach (['existing_project_id', 'special_zone_id'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'string', 'max:100'];
}
foreach (['province_id', 'district_id', 'admin_post_id', 'locality_id'] as $field) {
$rules['economic_snapshot.'.$field] = ['nullable', 'uuid', 'exists:geo_locations,id'];
}
foreach (['person_type', 'investor_origin', 'tax_residency_country', 'entity_stage', 'legal_form', 'commercial_name',
'registration_number', 'representative_name', 'representative_capacity', 'phone', 'address', 'contact_country',
'contact_province', 'contact_district', 'preferred_channel'] as $field) {
$rules['personal_snapshot.'.$field] = ['nullable', 'string', 'max:3000'];
}
return $rules;
}
public function withValidator(Validator $validator): void
{
$validator->after(function (Validator $validator): void {
$economic = $this->input('economic_snapshot', []);
if (! is_array($economic)) {
return;
}
$allowedLabels = [
'economic_snapshot.selection_labels' => ['purpose', 'manifestation_type', 'preferred_language', 'project_type', 'sector_id', 'activity_id',
'province_id', 'district_id', 'admin_post_id', 'locality_id', 'site_known', 'land_status', 'infrastructure_needs',
'area_unit', 'required_documents', 'opportunity_currency',
'currency', 'funds_origin_countries', 'environmental_category', 'tax_calculation', 'offer_or_demand',
'opportunity_type', 'confidentiality', 'publish_requested', 'terms_accepted', 'truth_declaration',
'reuse_simulation', 'contact_authorisation', 'publication_authorisation', 'public_fields_confirmation', 'validation_acknowledged'],
'personal_snapshot.selection_labels' => ['person_type', 'investor_origin', 'tax_residency_country', 'entity_stage', 'legal_form', 'preferred_channel'],
];
foreach ($allowedLabels as $path => $allowedFields) {
$snapshot = str_starts_with($path, 'personal_') ? $this->input('personal_snapshot.selection_labels', []) : ($economic['selection_labels'] ?? []);
foreach (is_array($snapshot) ? $snapshot : [] as $field => $label) {
$labels = is_array($label) ? $label : [$label];
if (! is_string($field) || ! in_array($field, $allowedFields, true) || count($labels) > 100
|| collect($labels)->contains(fn ($value) => ! is_string($value) || mb_strlen($value) > 255)) {
$validator->errors()->add($path, 'Os rótulos de selecção não são válidos.');
break 2;
}
}
}
if (isset($economic['activity_id'], $economic['sector_id'])) {
$matchesSector = DB::table('economic_activities as a')
->join('reference_catalog_versions as v', 'v.id', '=', 'a.catalog_version_id')
->where('a.id', $economic['activity_id'])->where('a.sector_id', $economic['sector_id'])
->where('a.status', 'published')->where('v.status', 'published')
->whereDate('a.valid_from', '<=', today())
->where(fn ($query) => $query->whereNull('a.valid_to')->orWhereDate('a.valid_to', '>', today()))->exists();
if (! $matchesSector) {
$validator->errors()->add('economic_snapshot.activity_id', 'A actividade não pertence ao sector seleccionado ou já não está publicada.');
}
} elseif (! empty($economic['activity_id'])) {
$validator->errors()->add('economic_snapshot.sector_id', 'Seleccione o sector antes da actividade económica.');
}
$parents = ['district_id' => ['province_id', 'district'], 'admin_post_id' => ['district_id', 'admin_post'], 'locality_id' => ['admin_post_id', 'locality']];
foreach ($parents as $child => [$parentField, $type]) {
if (empty($economic[$child])) {
continue;
}
$parentType = ['district' => 'province', 'admin_post' => 'district', 'locality' => 'admin_post'][$type];
$valid = ! empty($economic[$parentField]) && DB::table('geo_locations as child')
->join('geo_locations as parent', 'parent.id', '=', 'child.parent_id')
->where('child.id', $economic[$child])->where('child.location_type', $type)
->where('child.source_system', 'cartografia-workbook')->where('child.data_classification', 'public')->whereNull('child.deleted_at')
->where('parent.id', $economic[$parentField])->where('parent.location_type', $parentType)
->where('parent.source_system', 'cartografia-workbook')->where('parent.data_classification', 'public')->whereNull('parent.deleted_at')->exists();
if (! $valid) {
$validator->errors()->add('economic_snapshot.'.$child, 'Seleccione uma opção publicada dependente do nível territorial anterior.');
}
}
$financeKeys = ['total_investment', 'own_capital', 'domestic_debt', 'foreign_debt', 'grants'];
if (collect($financeKeys)->every(fn (string $key) => array_key_exists($key, $economic) && $economic[$key] !== null && $economic[$key] !== '')) {
$funding = (float) $economic['own_capital'] + (float) $economic['domestic_debt'] + (float) $economic['foreign_debt'] + (float) $economic['grants'];
if (abs($funding - (float) $economic['total_investment']) > 0.01) {
$validator->errors()->add('economic_snapshot.total_investment', 'A soma do capital, dívida e subvenções deve reconciliar com o investimento total.');
}
}
});
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreSimulationRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, list<mixed>> */
public function rules(): array
{
return [
'base' => ['required', 'numeric', 'min:0'],
'rate' => ['required', 'numeric', 'between:0,100'],
'reduction' => ['required', 'numeric', 'between:0,100'],
'organization_id' => ['nullable', 'uuid', 'exists:organizations,id'],
];
}
}
@@ -0,0 +1,24 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateSimulationScenarioRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, list<mixed>> */
public function rules(): array
{
return [
'label' => ['required', 'string', 'min:1', 'max:100'],
'base' => ['required', 'numeric', 'min:0'],
'rate' => ['required', 'numeric', 'between:0,100'],
'reduction' => ['required', 'numeric', 'between:0,100'],
];
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class PublicBusinessOpportunityResource extends JsonResource
{
/** @return array<string, mixed> */
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'slug' => $this->public_slug,
'title' => $this->public_title,
'activity_code' => $this->activity_code,
'stage' => $this->stage,
'objective' => $this->objective,
'currency' => $this->currency,
'amount_min' => $this->amount_min,
'amount_max' => $this->amount_max,
'available_percentage' => $this->available_percentage,
'published_at' => $this->published_at,
'sector' => $this->whenLoaded('sector', fn () => $this->sector?->only(['name', 'slug'])),
'province' => $this->province,
'district' => $this->district,
'locality' => $this->locality,
];
}
}
+110
View File
@@ -0,0 +1,110 @@
<?php
namespace App\Jobs;
use App\Models\Simulation;
use App\Services\SimulationExecution;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class RunSimulation implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 30;
public function __construct(public string $simulationId, public ?int $actorId, public ?string $tokenHash) {}
/** @return list<int> */
public function backoff(): array
{
return [10, 60];
}
public function handle(SimulationExecution $execution): void
{
$shouldRun = DB::transaction(function () use ($execution): bool {
$execution->setContext($this->actorId, $this->tokenHash);
$job = DB::table('simulation_jobs')->where('simulation_id', $this->simulationId)->where('job_type', 'execute')->lockForUpdate()->first();
if (! $job || in_array($job->status, ['succeeded', 'failed'], true)) {
return false;
}
DB::table('simulation_jobs')->where('id', $job->id)->update([
'status' => 'running', 'attempts' => min(3, max((int) $job->attempts + 1, (int) $this->attempts())),
'started_at' => now(), 'updated_at' => now(),
]);
return true;
});
if (! $shouldRun) {
return;
}
DB::transaction(function () use ($execution): void {
$execution->setContext($this->actorId, $this->tokenHash);
$simulation = Simulation::whereNull('deleted_at')->lockForUpdate()->find($this->simulationId);
if (! $simulation || $simulation->status === 'completed') {
return;
}
$job = DB::table('simulation_jobs')->where('simulation_id', $this->simulationId)->where('job_type', 'execute')->lockForUpdate()->first();
if (! $job || in_array($job->status, ['succeeded', 'failed'], true)) {
return;
}
$scenario = DB::table('simulation_scenarios')->where('simulation_id', $simulation->id)->orderBy('code')->first();
if (! $scenario) {
throw new \RuntimeException('Simulation has no scenario.');
}
$input = json_decode($scenario->parameters, true, flags: JSON_THROW_ON_ERROR);
$row = DB::selectOne('select (CAST(? AS numeric) * CAST(? AS numeric) / 100 * (1 - CAST(? AS numeric) / 100))::text as value', [$input['base'], $input['rate'], $input['reduction']]);
$result = ['base' => $input['base'], 'rate_percent' => $input['rate'], 'reduction_percent' => $input['reduction'], 'indicative_total' => $row->value,
'currency' => null, 'legal_interpretation' => false, 'formula_version' => 'portal-tax-arithmetic-v1'];
$json = json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$canonicalResult = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$json])->canonical;
$resultHash = hash('sha256', $canonicalResult);
$resultId = (string) Str::uuid();
$engineVersion = 'portal-tax-arithmetic-v1+simulation-v'.$simulation->version_no;
DB::table('simulation_results')->insert([
'id' => $resultId, 'simulation_id' => $simulation->id, 'scenario_id' => $scenario->id, 'engine_version' => $engineVersion,
'result_snapshot' => $json, 'result_sha256' => $resultHash, 'created_at' => now(),
]);
$simulation->update(['status' => 'completed', 'version_no' => $simulation->version_no + 1, 'result_snapshot' => $result, 'error_code' => null]);
DB::table('simulation_jobs')->where('id', $job->id)->update(['status' => 'succeeded', 'finished_at' => now(), 'updated_at' => now()]);
DB::table('simulation_reports')->insert(['id' => (string) Str::uuid(), 'simulation_id' => $simulation->id, 'version_no' => $scenario->version_no,
'status' => 'ready', 'report_snapshot' => $json, 'report_sha256' => $resultHash, 'created_at' => now(), 'updated_at' => now()]);
$sourceJson = json_encode($input, JSON_THROW_ON_ERROR);
$canonicalSource = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$sourceJson])->canonical;
DB::table('simulation_result_sources')->insertOrIgnore(['id' => (string) Str::uuid(), 'result_id' => $resultId,
'source_type' => 'user_input', 'source_id' => $scenario->id, 'source_version' => (string) $scenario->version_no,
'source_snapshot' => $sourceJson, 'source_sha256' => hash('sha256', $canonicalSource), 'created_at' => now()]);
DB::table('simulation_audit_events')->insert(['simulation_id' => $simulation->id, 'actor_user_id' => $simulation->owner_user_id,
'action' => 'simulation.completed', 'correlation_id' => $simulation->id, 'data' => json_encode(['result_sha256' => hash('sha256', $json), 'attempt' => $job->attempts + 1], JSON_THROW_ON_ERROR), 'occurred_at' => now()]);
}, 3);
}
public function failed(\Throwable $exception): void
{
DB::transaction(function (): void {
app(SimulationExecution::class)->setContext($this->actorId, $this->tokenHash);
$job = DB::table('simulation_jobs')->where('simulation_id', $this->simulationId)->where('job_type', 'execute')->lockForUpdate()->first();
if (! $job || $job->status === 'succeeded') {
return;
}
DB::table('simulation_jobs')->where('id', $job->id)->update(['status' => 'failed', 'attempts' => 3, 'finished_at' => now(), 'error_code' => 'execution_failed', 'updated_at' => now()]);
$simulation = Simulation::where('id', $this->simulationId)->lockForUpdate()->first();
if ($simulation) {
if ($simulation->status !== 'failed') {
$simulation->update(['status' => 'failed', 'version_no' => $simulation->version_no + 1, 'error_code' => 'execution_failed']);
}
DB::table('simulation_audit_events')->insert(['simulation_id' => $simulation->id, 'actor_user_id' => $this->actorId,
'action' => 'simulation.failed', 'correlation_id' => $simulation->id, 'data' => json_encode(['error_code' => 'execution_failed'], JSON_THROW_ON_ERROR), 'occurred_at' => now()]);
}
});
}
}
+29 -1
View File
@@ -2,9 +2,37 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ExistingBusiness extends DomainRecord
{
protected $hidden = ['payload'];
protected $casts = ['payload' => 'encrypted:array', 'version_no' => 'integer'];
protected $casts = [
'payload' => 'encrypted:array',
'version_no' => 'integer',
'amount_min' => 'decimal:2',
'amount_max' => 'decimal:2',
'available_percentage' => 'decimal:2',
'submitted_at' => 'immutable_datetime',
'published_at' => 'immutable_datetime',
'expires_at' => 'immutable_datetime',
'next_action_due_at' => 'immutable_datetime',
];
/** @return BelongsTo<Sector, $this> */
public function sector(): BelongsTo
{
return $this->belongsTo(Sector::class);
}
/** @param Builder<ExistingBusiness> $query
* @return Builder<ExistingBusiness>
*/
public function scopePublished(Builder $query): Builder
{
return $query->where('status', 'published')->whereNotNull('published_at')->where('published_at', '<=', now())
->where(fn (Builder $q) => $q->whereNull('expires_at')->orWhere('expires_at', '>=', today()));
}
}
+24 -1
View File
@@ -15,7 +15,8 @@ class InterestRequest extends Model
'investor_type', 'country', 'sector_interest',
'opportunity_reference', 'investment_range', 'message',
'privacy_consent', 'consented_at', 'status', 'source_page',
'ip_address', 'user_agent', 'metadata', 'reviewed_at', 'reviewed_by',
'ip_address', 'user_agent', 'metadata', 'reviewed_at', 'reviewed_by', 'existing_business_id', 'investor_id',
'modality', 'requested_percentage', 'ticket_min', 'ticket_max', 'ticket_currency', 'assigned_to', 'next_action', 'next_action_due_at',
];
protected $casts = [
@@ -23,6 +24,10 @@ class InterestRequest extends Model
'consented_at' => 'datetime',
'metadata' => 'array',
'reviewed_at' => 'datetime',
'requested_percentage' => 'decimal:2',
'ticket_min' => 'decimal:2',
'ticket_max' => 'decimal:2',
'next_action_due_at' => 'immutable_datetime',
];
/** @return BelongsTo<Opportunity, $this> */
@@ -30,4 +35,22 @@ class InterestRequest extends Model
{
return $this->belongsTo(Opportunity::class);
}
/** @return BelongsTo<ExistingBusiness, $this> */
public function existingBusiness(): BelongsTo
{
return $this->belongsTo(ExistingBusiness::class);
}
/** @return BelongsTo<Investor, $this> */
public function investor(): BelongsTo
{
return $this->belongsTo(Investor::class);
}
/** @return BelongsTo<User, $this> */
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_to');
}
}
+5
View File
@@ -34,6 +34,11 @@ class Investor extends Model
'created_at' => 'immutable_datetime',
'updated_at' => 'immutable_datetime',
'deleted_at' => 'immutable_datetime',
'investment_sectors' => 'array',
'investment_geographies' => 'array',
'investment_horizon_months' => 'integer',
'ticket_min' => 'decimal:2',
'ticket_max' => 'decimal:2',
];
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/** @property int $version_no */
class QuestionnaireVersion extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected function casts(): array
{
return ['schema_snapshot' => 'array', 'valid_from' => 'date', 'valid_to' => 'date'];
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property string $id
* @property int|null $owner_user_id
* @property string|null $organization_id
* @property string|null $anonymous_token_hash
* @property string $status
* @property int $version_no
* @property array<string, mixed> $input_snapshot
* @property array<string, mixed>|null $result_snapshot
*/
class Simulation extends Model
{
use SoftDeletes;
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = ['input_snapshot' => 'array', 'result_snapshot' => 'array', 'calculation_snapshot' => 'array'];
/** @return HasMany<SimulationScenario, $this> */
public function scenarios(): HasMany
{
return $this->hasMany(SimulationScenario::class);
}
/** @return HasMany<SimulationReport, $this> */
public function reports(): HasMany
{
return $this->hasMany(SimulationReport::class);
}
}
+35
View File
@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
class SimulationFormDraft extends Model
{
protected $table = 'simulation_form_drafts';
protected $fillable = [
'id', 'owner_user_id', 'anonymous_token_hash', 'version_no', 'create_key_hash', 'create_payload_hash', 'snapshot_sha256',
'personal_snapshot', 'economic_snapshot', 'deleted_at',
];
protected function casts(): array
{
return ['version_no' => 'integer', 'deleted_at' => 'immutable_datetime'];
}
/** @param array<string,mixed> $snapshot */
public static function encryptSnapshot(array $snapshot): string
{
return Crypt::encryptString(json_encode($snapshot, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE));
}
/** @return array<string,mixed> */
public static function decryptSnapshot(string $snapshot): array
{
$decoded = json_decode(Crypt::decryptString($snapshot), true, 32, JSON_THROW_ON_ERROR);
return is_array($decoded) ? $decoded : [];
}
}
+14
View File
@@ -0,0 +1,14 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SimulationJob extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SimulationObligation extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = ['snapshot' => 'array'];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SimulationReport extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = ['report_snapshot' => 'array'];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SimulationResult extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = ['result_snapshot' => 'array'];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SimulationResultSource extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = ['source_snapshot' => 'array'];
}
+16
View File
@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SimulationScenario extends Model
{
public $incrementing = false;
protected $keyType = 'string';
protected $guarded = [];
protected $casts = ['parameters' => 'array'];
}
+52 -5
View File
@@ -19,7 +19,14 @@ class ExistingBusinessPolicy
public function view(User $user, ExistingBusiness $business): bool
{
return $this->owns($user, $business) || $this->review($user, $business);
if ($this->owns($user, $business) || $this->review($user, $business) || $this->triage($user, $business)
|| $this->approveLevelOne($user, $business) || $this->approveLevelTwo($user, $business) || $this->publish($user, $business)) {
return true;
}
return ! $business->trashed() && $business->owner_user_id !== $user->id
&& in_array($business->status, ['rejected', 'published', 'suspended', 'cancelled', 'archived'], true)
&& $this->reviewQueue($user);
}
public function delete(User $user, ExistingBusiness $business): bool
@@ -35,13 +42,53 @@ class ExistingBusinessPolicy
public function review(User $user, ExistingBusiness $business): bool
{
return ! $business->trashed() && $business->owner_user_id !== $user->id
&& in_array($business->status, ['submitted', 'in_review', 'needs_information'], true)
&& $this->reviewQueue($user);
&& in_array($business->status, ['triage', 'in_review', 'needs_information'], true)
&& $this->designated($user, 'reviewers');
}
public function triage(User $user, ExistingBusiness $business): bool
{
return ! $business->trashed() && $business->owner_user_id !== $user->id
&& in_array($business->status, ['submitted', 'triage'], true)
&& $this->designated($user, 'triagers');
}
public function approveLevelOne(User $user, ExistingBusiness $business): bool
{
return ! $business->trashed() && $business->owner_user_id !== $user->id
&& $business->status === 'approval_l1'
&& $this->designated($user, 'approvers_l1');
}
public function approveLevelTwo(User $user, ExistingBusiness $business): bool
{
return ! $business->trashed() && $business->owner_user_id !== $user->id
&& $business->status === 'approval_l2'
&& $this->designated($user, 'approvers_l2');
}
public function publish(User $user, ExistingBusiness $business): bool
{
return ! $business->trashed() && $business->owner_user_id !== $user->id
&& $business->status === 'approved'
&& $this->designated($user, 'publishers');
}
public function reviewQueue(User $user): bool
{
return $user->can('manage-portal') && (bool) $user->two_factor_confirmed_at && (bool) $user->two_factor_secret
&& in_array(strtolower($user->email), config('existing-business.reviewers', []), true);
foreach (['reviewers', 'triagers', 'approvers_l1', 'approvers_l2', 'publishers'] as $designation) {
if ($this->designated($user, $designation)) {
return true;
}
}
return false;
}
private function designated(User $user, string $designation): bool
{
return $user->hasVerifiedEmail() && $user->can('manage-portal')
&& (bool) $user->two_factor_confirmed_at && (bool) $user->two_factor_secret
&& in_array(strtolower($user->email), config('existing-business.'.$designation, []), true);
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace App\Policies;
use App\Models\Simulation;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class SimulationPolicy
{
public function view(User $user, Simulation $simulation): bool
{
if ($simulation->organization_id === null && $simulation->owner_user_id === $user->id && $user->hasVerifiedEmail()) {
return true;
}
return $simulation->organization_id !== null && $user->hasVerifiedEmail()
&& DB::table('memberships')->where('user_id', $user->id)
->where('organization_id', $simulation->organization_id)->whereNull('deleted_at')->exists();
}
public function update(User $user, Simulation $simulation): bool
{
if (! $user->hasVerifiedEmail() || ! in_array($simulation->status, ['queued', 'completed', 'failed'], true)) {
return false;
}
if ($simulation->organization_id === null) {
return $simulation->owner_user_id === $user->id;
}
return DB::table('memberships')->where('user_id', $user->id)->where('organization_id', $simulation->organization_id)
->whereNull('deleted_at')->whereIn('role', ['admin', 'manager', 'officer'])->exists();
}
}
+39 -1
View File
@@ -49,7 +49,45 @@ class AppServiceProvider extends ServiceProvider
$model::deleted($invalidate);
}
RateLimiter::for('api', fn (Request $request) => Limit::perMinute(60)->by($request->ip()));
$requestLimitKey = static function (Request $request): string {
if ($user = $request->user()) {
return 'user:'.$user->getAuthIdentifier();
}
$token = $request->header('X-Simulation-Token');
return is_string($token) && $token !== ''
? 'anonymous:'.hash('sha256', $token)
: 'ip:'.$request->ip();
};
RateLimiter::for('api', function (Request $request) use ($requestLimitKey) {
return Limit::perMinute(60)->by($requestLimitKey($request))
->response(fn (Request $request, array $headers) => response()->json([
'message' => 'Demasiadas solicitações. Aguarde um pouco e tente novamente.',
], 429, $headers));
});
RateLimiter::for('simulation-draft-writes', function (Request $request) use ($requestLimitKey) {
return Limit::perMinute(30)->by('simulation-draft:'.$requestLimitKey($request))
->response(fn (Request $request, array $headers) => response()->json([
'message' => 'Foram guardadas demasiadas alterações em pouco tempo. Aguarde um minuto e tente novamente.',
], 429, $headers));
});
RateLimiter::for('simulation-calculations', function (Request $request) use ($requestLimitKey) {
return Limit::perMinute(10)->by('simulation-calculation:'.$requestLimitKey($request))
->response(fn (Request $request, array $headers) => response()->json([
'message' => 'Foram solicitados demasiados cálculos em pouco tempo. Aguarde antes de tentar novamente.',
], 429, $headers));
});
RateLimiter::for('simulation-scenario-updates', function (Request $request) use ($requestLimitKey) {
return Limit::perMinute(10)->by('simulation-scenario-update:'.$requestLimitKey($request))
->response(fn (Request $request, array $headers) => response()->json([
'message' => 'Foram solicitadas demasiadas alterações de cenário em pouco tempo. Aguarde antes de tentar novamente.',
], 429, $headers));
});
}
/**
+14
View File
@@ -23,6 +23,15 @@ final class ExistingBusinessDrafts
$data = Validator::make($input, [
'modality' => ['required', 'in:'.implode(',', self::MODALITIES)],
'sector_id' => ['sometimes', 'nullable', 'integer', Rule::exists('sectors', 'id')->whereNull('deleted_at')->where('is_active', true)],
'public_title' => ['sometimes', 'nullable', 'string', 'max:180'],
'activity_code' => ['sometimes', 'nullable', 'string', 'max:60'],
'stage' => ['sometimes', 'nullable', 'string', 'max:40'],
'objective' => ['sometimes', 'nullable', 'string', 'max:5000'],
'currency' => ['sometimes', 'nullable', 'regex:/^[A-Z]{3}$/', 'required_with:amount_min,amount_max'],
'amount_min' => ['sometimes', 'nullable', 'numeric', 'min:0', 'max:999999999999999999.99'],
'amount_max' => ['sometimes', 'nullable', 'numeric', 'min:0', 'gte:amount_min', 'max:999999999999999999.99'],
'available_percentage' => ['sometimes', 'nullable', 'numeric', 'between:0,100'],
'funds_use' => ['sometimes', 'nullable', 'string', 'max:10000'],
'classification' => ['required', 'in:internal,confidential,restricted'],
'version_no' => [$id ? 'required' : 'prohibited', 'integer', 'min:1'],
'reason' => ['required', 'string', 'min:10', 'max:1000'],
@@ -56,6 +65,11 @@ final class ExistingBusinessDrafts
$business->status = 'draft';
}
$business->fill(['modality' => $data['modality'], 'classification' => $data['classification'], 'payload' => $data['payload']]);
foreach (['public_title', 'activity_code', 'stage', 'objective', 'currency', 'amount_min', 'amount_max', 'available_percentage', 'funds_use'] as $field) {
if (array_key_exists($field, $data)) {
$business->{$field} = $data[$field];
}
}
if (array_key_exists('sector_id', $data)) {
$business->sector_id = $data['sector_id'];
}
+98 -4
View File
@@ -2,6 +2,7 @@
namespace App\Services;
use App\Domain\Opportunities\Enums\OpportunityStatus;
use App\Models\ExistingBusiness;
use App\Models\ExistingBusinessObject;
use App\Models\User;
@@ -19,16 +20,49 @@ final class ExistingBusinessIntake
public function transition(User $actor, string $id, int $version, string $action, string $note, bool $consent = false, ?string $confirmationCode = null): ExistingBusiness
{
Validator::make(compact('action', 'note'), [
'action' => ['required', 'in:submitted,in_review,needs_information,withdrawn'],
'action' => ['required', Rule::in([
OpportunityStatus::Submitted->value,
OpportunityStatus::Triage->value,
OpportunityStatus::InReview->value,
OpportunityStatus::NeedsInformation->value,
OpportunityStatus::ApprovalL1->value,
OpportunityStatus::ApprovalL2->value,
OpportunityStatus::Approved->value,
OpportunityStatus::Published->value,
OpportunityStatus::Rejected->value,
OpportunityStatus::Withdrawn->value,
])],
'note' => ['required', 'string', 'min:10', 'max:2000'],
])->validate();
return DB::transaction(function () use ($actor, $id, $version, $action, $note, $consent, $confirmationCode) {
$business = app(BusinessRepository::class)->find($id, true);
Gate::forUser($actor)->authorize(in_array($action, ['submitted', 'withdrawn'], true) ? 'owns' : 'review', $business);
$ability = match ($action) {
OpportunityStatus::Submitted->value, OpportunityStatus::Withdrawn->value => 'owns',
OpportunityStatus::Triage->value => 'triage',
OpportunityStatus::ApprovalL2->value => 'approveLevelOne',
OpportunityStatus::Approved->value => 'approveLevelTwo',
OpportunityStatus::Published->value => 'publish',
OpportunityStatus::Rejected->value => in_array($business->status, ['approval_l1', 'approval_l2'], true)
? ($business->status === 'approval_l1' ? 'approveLevelOne' : 'approveLevelTwo') : 'review',
default => 'review',
};
Gate::forUser($actor)->authorize($ability, $business);
abort_unless($business->version_no === $version, 409, 'Versão desactualizada.');
$allowed = ['submitted' => ['draft', 'needs_information'], 'in_review' => ['submitted'],
'needs_information' => ['in_review'], 'withdrawn' => ['draft', 'submitted', 'in_review', 'needs_information']];
$allowed = [
OpportunityStatus::Submitted->value => [OpportunityStatus::Draft->value, OpportunityStatus::NeedsInformation->value],
OpportunityStatus::Triage->value => [OpportunityStatus::Submitted->value],
OpportunityStatus::InReview->value => [OpportunityStatus::Triage->value],
OpportunityStatus::NeedsInformation->value => [OpportunityStatus::Triage->value, OpportunityStatus::InReview->value],
OpportunityStatus::ApprovalL1->value => [OpportunityStatus::InReview->value],
OpportunityStatus::ApprovalL2->value => [OpportunityStatus::ApprovalL1->value],
OpportunityStatus::Approved->value => [OpportunityStatus::ApprovalL2->value],
OpportunityStatus::Published->value => [OpportunityStatus::Approved->value],
OpportunityStatus::Rejected->value => [OpportunityStatus::Triage->value, OpportunityStatus::InReview->value,
OpportunityStatus::ApprovalL1->value, OpportunityStatus::ApprovalL2->value],
OpportunityStatus::Withdrawn->value => [OpportunityStatus::Draft->value, OpportunityStatus::Submitted->value,
OpportunityStatus::Triage->value, OpportunityStatus::InReview->value, OpportunityStatus::NeedsInformation->value],
];
abort_unless(in_array($business->status, $allowed[$action], true), 409, 'Transição indisponível neste estado.');
if ($action === 'submitted') {
if (($business->payload['wizard_version'] ?? null) === '1') {
@@ -50,9 +84,69 @@ final class ExistingBusinessIntake
'valid_until' => ['required', 'date_format:Y-m-d', 'after_or_equal:today'], 'consent' => ['accepted'],
])->validate();
}
if ($action === OpportunityStatus::Approved->value) {
$levelOneActor = DB::table('business_workflow_transitions')->where('existing_business_id', $business->id)
->where('to_status', OpportunityStatus::ApprovalL2->value)->orderByDesc('created_at')->value('actor_id');
abort_unless($levelOneActor !== null && (int) $levelOneActor !== (int) $actor->id, 403,
'A aprovação de nível 2 deve ser feita por uma pessoa diferente da aprovação de nível 1.');
}
if ($action === OpportunityStatus::Published->value) {
abort_unless(is_string($business->public_title) && trim($business->public_title) !== '' && $business->sector_id,
422, 'Preencha o título público e o sector antes de publicar.');
$approvalActors = DB::table('business_workflow_transitions')->where('existing_business_id', $business->id)
->whereIn('to_status', [OpportunityStatus::ApprovalL2->value, OpportunityStatus::Approved->value])
->whereNotNull('actor_id')->pluck('actor_id')->map(fn ($id) => (int) $id)->all();
abort_unless(! in_array((int) $actor->id, $approvalActors, true), 403,
'A publicação deve ser executada por uma pessoa diferente dos aprovadores.');
}
$fromStatus = $business->status;
$business->status = $action;
if ($action === OpportunityStatus::Published->value) {
$business->published_at = now();
$business->public_slug ??= Str::slug($business->public_title).'-'.strtolower(substr(str_replace('-', '', $business->id), 0, 10));
}
$business->version_no++;
$business->save();
$correlationId = (string) Str::uuid();
DB::table('business_workflow_transitions')->insert([
'id' => (string) Str::uuid(),
'existing_business_id' => $business->id,
'from_status' => $fromStatus,
'to_status' => $action,
'actor_id' => $actor->id,
'reason' => Crypt::encryptString($note),
'correlation_id' => $correlationId,
'metadata' => json_encode(['version_no' => $business->version_no, 'source' => 'portal-intake'], JSON_THROW_ON_ERROR),
'created_at' => now(),
]);
if ($action === OpportunityStatus::Submitted->value) {
DB::table('tasks')->insertOrIgnore([
'source_system' => 'portal-intake',
'external_id' => 'BusinessListingTriage:'.$business->id.':'.$business->version_no,
'version_no' => 1,
'data_classification' => 'internal',
'correlation_id' => $correlationId,
'existing_business_id' => $business->id,
'task_type' => 'triage',
'title' => 'Triar oportunidade '.$business->code,
'status' => 'open',
'priority' => 3,
'due_at' => now()->addWeekdays(3),
'created_at' => now(),
'updated_at' => now(),
]);
DB::table('outbox_events')->insert([
'source_system' => 'portal-intake',
'external_id' => 'BusinessListingSubmitted:'.$business->id.':'.$business->version_no,
'aggregate_type' => 'business_listing',
'aggregate_id' => $business->id,
'event_type' => 'BusinessListingSubmitted',
'data_classification' => 'confidential',
'correlation_id' => $correlationId,
'status' => 'pending',
'payload' => json_encode(['business_id' => $business->id, 'version_no' => $business->version_no], JSON_THROW_ON_ERROR),
]);
}
DB::table('existing_business_reviews')->insert(['id' => (string) Str::uuid(), 'business_id' => $id, 'actor_id' => $actor->id,
'version_no' => $business->version_no, 'action' => $action,
'note' => Crypt::encryptString(json_encode(['note' => $note, 'review_consent' => $action === 'submitted' ? $consent : null], JSON_THROW_ON_ERROR))]);
+27 -2
View File
@@ -16,7 +16,8 @@ final class InterestSubmission
*/
public function receive(#[SensitiveParameter] array $payload, #[SensitiveParameter] ?string $key, Closure $create): InterestRequest
{
$canonical = array_replace(['phone' => null, 'opportunityReference' => null, 'investmentRange' => null], $payload);
$canonical = array_replace(['phone' => null, 'opportunityReference' => null, 'existingBusinessId' => null,
'modality' => null, 'investmentRange' => null, 'shareWithProponent' => false], $payload);
$canonical['email'] = strtolower(trim($canonical['email']));
$canonical['fullName'] = trim($canonical['fullName']);
$canonical['message'] = trim($canonical['message']);
@@ -25,7 +26,7 @@ final class InterestSubmission
$payloadHash = hash_hmac('sha256', json_encode($canonical, JSON_THROW_ON_ERROR), config()->string('app.key'));
$keyHash = $key === null ? null : hash('sha256', strtolower($key));
return DB::transaction(function () use ($keyHash, $payloadHash, $create): InterestRequest {
return DB::transaction(function () use ($keyHash, $payloadHash, $create, $canonical): InterestRequest {
if ($keyHash !== null) {
// Serialise concurrent submissions of this key across all web workers.
DB::select('select pg_advisory_xact_lock(250925, hashtext(?))', [$keyHash]);
@@ -41,6 +42,10 @@ final class InterestSubmission
$interest = $create();
$integrationId = (string) Str::uuid();
$interest->forceFill(['integration_id' => $integrationId])->save();
$this->recordConsent($interest, $integrationId, 'respond_to_interest', config('existing-business.consent_policy_version'));
if ($canonical['shareWithProponent'] === true || $canonical['shareWithProponent'] === 1 || $canonical['shareWithProponent'] === '1') {
$this->recordConsent($interest, $integrationId, 'share_contact_with_proponent', config('existing-business.sharing_policy_version'));
}
DB::table('outbox_events')->insert([
'source_system' => 'portal-intake',
'external_id' => 'ExpressionOfInterestSubmitted:'.$integrationId,
@@ -59,4 +64,24 @@ final class InterestSubmission
return $interest;
});
}
private function recordConsent(InterestRequest $interest, string $correlationId, string $purpose, string $policyVersion): void
{
DB::table('consents')->insert([
'id' => (string) Str::uuid(),
'source_system' => 'portal-intake',
'external_id' => 'InterestConsent:'.$interest->integration_id.':'.$purpose,
'version_no' => 1,
'data_classification' => 'confidential',
'correlation_id' => $correlationId,
'subject_ref' => $interest->reference,
'purpose' => $purpose,
'status' => 'granted',
'granted_at' => now(),
'interest_request_id' => $interest->id,
'policy_version' => $policyVersion,
'created_at' => now(),
'updated_at' => now(),
]);
}
}
+108
View File
@@ -0,0 +1,108 @@
<?php
namespace App\Services;
use App\Domain\Interests\Enums\InterestStatus;
use App\Models\InterestRequest;
use App\Models\User;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
final class InterestWorkflow
{
/** @var array<string, list<string>> */
private const TRANSITIONS = [
'received' => ['in_review', 'qualification', 'contacted', 'closed', 'cancelled', 'refused'],
'submitted' => ['in_review', 'qualification', 'contacted', 'closed', 'cancelled', 'refused'],
'in_review' => ['qualification', 'qualified', 'contacted', 'referred', 'closed', 'cancelled', 'refused'],
'reviewed' => ['qualification', 'qualified', 'contacted', 'referred', 'closed', 'cancelled', 'refused'],
'qualification' => ['qualified', 'contacted', 'referred', 'closed', 'cancelled', 'refused'],
'qualified' => ['contacted', 'contact_accepted', 'due_diligence', 'referred', 'closed', 'cancelled', 'refused'],
'contacted' => ['contact_accepted', 'due_diligence', 'negotiation', 'referred', 'closed', 'cancelled', 'refused'],
'contact_accepted' => ['due_diligence', 'negotiation', 'closed', 'cancelled', 'refused'],
'referred' => ['in_review', 'contacted', 'closed', 'cancelled'],
'due_diligence' => ['negotiation', 'closed', 'cancelled', 'refused'],
'negotiation' => ['converted', 'closed', 'cancelled', 'refused'],
'pending' => ['in_review', 'contacted', 'closed', 'cancelled', 'refused'],
'closed' => ['in_review'],
'cancelled' => [],
'refused' => [],
'converted' => [],
];
public function transition(User $actor, InterestRequest $interest, string $status, ?string $note = null, ?string $nextAction = null, ?string $dueAt = null, ?int $assigneeId = null, bool $assignmentProvided = false, bool $actionProvided = false): InterestRequest
{
if (! in_array($status, array_column(InterestStatus::cases(), 'value'), true)) {
throw ValidationException::withMessages(['status' => 'Estado inválido.']);
}
return DB::transaction(function () use ($actor, $interest, $status, $note, $nextAction, $dueAt, $assigneeId, $assignmentProvided, $actionProvided) {
$locked = InterestRequest::query()->lockForUpdate()->findOrFail($interest->id);
$from = $locked->status;
$actionDate = $dueAt ? Carbon::parse($dueAt)->toDateString() : null;
$followUpChanged = ($actionProvided && ($locked->next_action !== $nextAction
|| $locked->next_action_due_at?->toDateString() !== $actionDate))
|| ($assignmentProvided && $locked->assigned_to !== $assigneeId);
if ($from !== $status && ! in_array($status, self::TRANSITIONS[$from] ?? [], true)) {
throw ValidationException::withMessages(['status' => 'Esta transição não é permitida a partir do estado actual.']);
}
$locked->forceFill([
'status' => $status,
'reviewed_at' => now(),
'reviewed_by' => $actor->id,
...($actionProvided ? ['next_action' => $nextAction, 'next_action_due_at' => $dueAt] : []),
...($assignmentProvided ? ['assigned_to' => $assigneeId] : []),
])->save();
if ($followUpChanged) {
DB::table('tasks')->where('interest_request_id', $locked->id)->where('task_type', 'interest_follow_up')
->whereIn('status', ['open', 'in_progress'])->update([
'status' => 'cancelled', 'completed_at' => now(), 'outcome' => 'superseded', 'updated_at' => now(),
]);
}
if ($followUpChanged && $locked->next_action && $locked->next_action_due_at) {
$correlationId = (string) Str::uuid();
$assignee = $assignmentProvided ? $assigneeId : $locked->assigned_to;
$externalId = 'InterestFollowUp:'.$locked->id.':'.hash('sha256', $locked->next_action.'|'.$locked->next_action_due_at.'|'.($assignee ?? ''));
DB::table('tasks')->insertOrIgnore([
'source_system' => 'portal-interest',
'external_id' => $externalId,
'version_no' => 1,
'data_classification' => 'confidential',
'correlation_id' => $correlationId,
'assignee_user_id' => $assignee,
'interest_request_id' => $locked->id,
'task_type' => 'interest_follow_up',
'title' => mb_substr($locked->next_action, 0, 200),
'status' => 'open',
'priority' => 3,
'due_at' => Carbon::parse($locked->next_action_due_at)->endOfDay(),
'created_at' => now(),
'updated_at' => now(),
]);
}
if ($from !== $status) {
$correlationId = (string) Str::uuid();
DB::table('business_workflow_transitions')->insert([
'id' => (string) Str::uuid(),
'interest_request_id' => $locked->id,
'from_status' => $from,
'to_status' => $status,
'actor_id' => $actor->id,
'reason' => $note ? Crypt::encryptString($note) : null,
'correlation_id' => $correlationId,
'metadata' => json_encode(['source' => 'management-interest'], JSON_THROW_ON_ERROR),
'created_at' => now(),
]);
}
return $locked->refresh();
});
}
}
+145
View File
@@ -0,0 +1,145 @@
<?php
namespace App\Services;
use App\Jobs\RunSimulation;
use App\Models\Simulation;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;
final class SimulationExecution
{
/**
* @param array{base: int|float|string, rate: int|float|string, reduction: int|float|string, organization_id?: string|null} $data
* @return array{simulation: Simulation, token: string|null, replayed: bool}
*/
public function create(array $data, ?int $actorId, ?string $organizationId, ?string $token, string $key): array
{
$tokenHash = $actorId === null ? hash_hmac('sha256', (string) $token, (string) config('app.key')) : null;
$scope = $actorId !== null ? (string) $actorId : ($tokenHash ?? 'anonymous');
$scopeHash = hash('sha256', $scope);
$keyHash = hash('sha256', strtolower($key));
$payload = ['base' => (string) $data['base'], 'rate' => (string) $data['rate'], 'reduction' => (string) $data['reduction'], 'organization_id' => $organizationId];
ksort($payload);
$payloadHash = hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
return DB::transaction(function () use ($actorId, $organizationId, $token, $tokenHash, $scopeHash, $keyHash, $payloadHash, $payload): array {
$this->setContext($actorId, $tokenHash);
DB::select('select pg_advisory_xact_lock(261001, hashtext(?))', [$scopeHash.$keyHash]);
$receipt = DB::table('simulation_idempotency')->where('scope_hash', $scopeHash)->where('key_hash', $keyHash)->first();
if ($receipt) {
if (! hash_equals($receipt->payload_hash, $payloadHash)) {
throw new HttpException(409, 'Idempotency-Key já foi utilizada com outros parâmetros.');
}
return ['simulation' => Simulation::with(['scenarios', 'reports'])->whereKey((string) $receipt->simulation_id)->firstOrFail(), 'token' => null, 'replayed' => true];
}
$plainToken = $actorId === null ? $token : null;
$tokenHash = $actorId === null ? hash_hmac('sha256', $plainToken, (string) config('app.key')) : null;
$this->setContext($actorId, $tokenHash);
$id = (string) Str::uuid();
$simulation = Simulation::create([
'id' => $id, 'questionnaire_version_id' => '61000100-0000-4000-8000-000000000001', 'owner_user_id' => $actorId, 'organization_id' => $organizationId,
'anonymous_token_hash' => $tokenHash, 'status' => 'queued', 'version_no' => 1,
'input_snapshot' => $payload, 'calculation_snapshot' => ['engine' => 'portal-tax-arithmetic-v1', 'formula' => 'base * rate / 100 * (1 - reduction / 100)', 'legal_rate_source' => null],
]);
$scenarioId = (string) Str::uuid();
DB::table('simulation_scenarios')->insert([
'id' => $scenarioId, 'simulation_id' => $id, 'code' => 'baseline', 'label' => 'Cenário base', 'version_no' => 1,
'parameters' => json_encode($payload, JSON_THROW_ON_ERROR), 'created_at' => now(), 'updated_at' => now(),
]);
$jobId = (string) Str::uuid();
DB::table('simulation_jobs')->insert(['id' => $jobId, 'simulation_id' => $id, 'job_type' => 'execute', 'status' => 'pending', 'created_at' => now(), 'updated_at' => now()]);
DB::table('simulation_idempotency')->insert([
'scope_hash' => $scopeHash, 'key_hash' => $keyHash, 'payload_hash' => $payloadHash,
'simulation_id' => $id, 'response_snapshot' => json_encode(['simulation_id' => $id], JSON_THROW_ON_ERROR), 'created_at' => now(),
]);
DB::table('simulation_audit_events')->insert([
'simulation_id' => $id, 'actor_user_id' => $actorId, 'action' => 'simulation.created',
'correlation_id' => $id, 'data' => json_encode(['status' => 'queued', 'scenario_count' => 1], JSON_THROW_ON_ERROR), 'occurred_at' => now(),
]);
$simulation->load(['scenarios', 'reports']);
RunSimulation::dispatch($id, $actorId, $tokenHash)->afterCommit();
return ['simulation' => $simulation, 'token' => $plainToken, 'replayed' => false];
}, 3);
}
public function find(string $id, Request $request): Simulation
{
return DB::transaction(function () use ($id, $request): Simulation {
$this->setContext($request->user()?->id, $this->tokenHash($request));
return Simulation::with(['scenarios', 'reports'])->whereNull('deleted_at')->findOrFail($id);
});
}
/** @param array{label: string, base: int|float|string, rate: int|float|string, reduction: int|float|string} $data */
public function updateScenario(string $id, string $scenarioId, array $data, Request $request): Simulation
{
return DB::transaction(function () use ($id, $scenarioId, $data, $request): Simulation {
$this->setContext($request->user()?->id, $this->tokenHash($request));
$simulation = Simulation::whereNull('deleted_at')->lockForUpdate()->findOrFail($id);
$this->requireMatch($request, $simulation);
$scenario = DB::table('simulation_scenarios')->where('simulation_id', $id)->where('id', $scenarioId)->lockForUpdate()->first();
abort_if($scenario === null, 404);
$parameters = ['base' => (string) $data['base'], 'rate' => (string) $data['rate'], 'reduction' => (string) $data['reduction']];
DB::table('simulation_scenarios')->where('id', $scenarioId)->update([
'label' => $data['label'], 'parameters' => json_encode($parameters, JSON_THROW_ON_ERROR), 'version_no' => $scenario->version_no + 1, 'updated_at' => now(),
]);
$simulation->update(['version_no' => $simulation->version_no + 1, 'status' => 'queued', 'input_snapshot' => $parameters, 'result_snapshot' => null, 'error_code' => null]);
DB::table('simulation_jobs')->where('simulation_id', $id)->where('job_type', 'execute')->update([
'status' => 'pending', 'attempts' => 0, 'available_at' => now(), 'started_at' => null, 'finished_at' => null, 'error_code' => null, 'updated_at' => now(),
]);
DB::table('simulation_audit_events')->insert([
'simulation_id' => $id, 'actor_user_id' => $request->user()?->id, 'action' => 'scenario.updated', 'correlation_id' => $id,
'data' => json_encode(['scenario_id' => $scenarioId, 'version_no' => $scenario->version_no + 1], JSON_THROW_ON_ERROR), 'occurred_at' => now(),
]);
RunSimulation::dispatch($id, $simulation->owner_user_id, $simulation->anonymous_token_hash)->afterCommit();
return $simulation->refresh()->load(['scenarios', 'reports']);
}, 3);
}
public function tokenHash(Request $request): ?string
{
$token = $request->header('X-Simulation-Token');
return is_string($token) && $token !== '' ? hash_hmac('sha256', $token, (string) config('app.key')) : null;
}
public function setContext(?int $actorId, ?string $tokenHash): void
{
DB::select("select set_config('app.actor_user_id', ?, true), set_config('app.simulation_token_hash', ?, true)", [(string) ($actorId ?? ''), (string) ($tokenHash ?? '')]);
}
private function requireMatch(Request $request, Simulation $simulation): void
{
$provided = $request->header('If-Match');
if (! $provided) {
throw new HttpException(428, 'If-Match é obrigatório.');
}
if (! hash_equals($this->etag($simulation), trim($provided))) {
throw new HttpException(412, 'A simulação foi alterada desde a última leitura.');
}
}
public function etag(Simulation $simulation): string
{
return '"sim-'.$simulation->id.'-v'.$simulation->version_no.'"';
}
public function authorizeOrganization(?int $actorId, ?string $organizationId): void
{
if ($organizationId === null) {
return;
}
abort_unless($actorId !== null && DB::table('memberships')->where('user_id', $actorId)->where('organization_id', $organizationId)
->whereNull('deleted_at')->whereIn('role', ['admin', 'manager', 'officer'])->exists(), 403);
}
}
+162
View File
@@ -0,0 +1,162 @@
<?php
namespace App\Services;
use App\Models\SimulationFormDraft;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;
final class SimulationFormDrafts
{
/** @param array{personal_snapshot: array<string,mixed>, economic_snapshot: array<string,mixed>} $data */
public function create(array $data, Request $request, string $key): array
{
[$actorId, $tokenHash] = $this->context($request);
$scope = $actorId === null ? 'anonymous:'.$tokenHash : 'user:'.$actorId;
$keyHash = hash_hmac('sha256', strtolower($key), (string) config('app.key'));
$canonicalPayload = json_encode($this->canonical($data), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
$payloadHash = hash('sha256', $canonicalPayload);
return DB::transaction(function () use ($actorId, $tokenHash, $scope, $keyHash, $payloadHash, $canonicalPayload, $data): array {
$this->setContext($actorId, $tokenHash);
DB::select('select pg_advisory_xact_lock(261010, hashtext(?))', [$scope]);
$query = SimulationFormDraft::query()->whereNull('deleted_at')->lockForUpdate();
$query = $actorId === null ? $query->where('anonymous_token_hash', $tokenHash) : $query->where('owner_user_id', $actorId);
$draft = $query->first();
if ($draft) {
if (hash_equals($draft->create_key_hash, $keyHash)) {
abort_unless(hash_equals($draft->create_payload_hash, $payloadHash), 409, 'Idempotency-Key já foi utilizada com outros dados.');
return ['draft' => $draft, 'replayed' => true];
}
throw new HttpException(409, 'Já existe um rascunho activo. Carregue-o antes de guardar novas alterações.');
}
$draft = SimulationFormDraft::query()->create([
'id' => (string) Str::uuid(), 'owner_user_id' => $actorId, 'anonymous_token_hash' => $tokenHash,
'version_no' => 1, 'create_key_hash' => $keyHash, 'create_payload_hash' => $payloadHash, 'snapshot_sha256' => $payloadHash,
'personal_snapshot' => SimulationFormDraft::encryptSnapshot($data['personal_snapshot']),
'economic_snapshot' => SimulationFormDraft::encryptSnapshot($data['economic_snapshot']),
]);
return ['draft' => $draft, 'replayed' => false];
}, 3);
}
/** @param array{personal_snapshot: array<string,mixed>, economic_snapshot: array<string,mixed>} $data */
public function update(string $id, array $data, Request $request): SimulationFormDraft
{
[$actorId, $tokenHash] = $this->context($request);
return DB::transaction(function () use ($id, $data, $request, $actorId, $tokenHash): SimulationFormDraft {
$this->setContext($actorId, $tokenHash);
$draft = SimulationFormDraft::query()->whereNull('deleted_at')->lockForUpdate()->findOrFail($id);
$this->requireMatch($request, $draft);
$canonicalPayload = json_encode($this->canonical($data), JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
$draft->update([
'personal_snapshot' => SimulationFormDraft::encryptSnapshot($data['personal_snapshot']),
'economic_snapshot' => SimulationFormDraft::encryptSnapshot($data['economic_snapshot']),
'snapshot_sha256' => hash('sha256', $canonicalPayload), 'version_no' => $draft->version_no + 1,
]);
return $draft->refresh();
}, 3);
}
public function current(Request $request): ?SimulationFormDraft
{
[$actorId, $tokenHash] = $this->context($request);
return DB::transaction(function () use ($actorId, $tokenHash): ?SimulationFormDraft {
$this->setContext($actorId, $tokenHash);
$query = SimulationFormDraft::query()->whereNull('deleted_at');
return ($actorId === null ? $query->where('anonymous_token_hash', $tokenHash) : $query->where('owner_user_id', $actorId))->first();
});
}
public function find(string $id, Request $request): SimulationFormDraft
{
[$actorId, $tokenHash] = $this->context($request);
return DB::transaction(function () use ($id, $actorId, $tokenHash): SimulationFormDraft {
$this->setContext($actorId, $tokenHash);
return SimulationFormDraft::query()->whereNull('deleted_at')->findOrFail($id);
});
}
public function delete(string $id, Request $request): void
{
[$actorId, $tokenHash] = $this->context($request);
DB::transaction(function () use ($id, $request, $actorId, $tokenHash): void {
$this->setContext($actorId, $tokenHash);
$draft = SimulationFormDraft::query()->whereNull('deleted_at')->lockForUpdate()->findOrFail($id);
$this->requireMatch($request, $draft);
$draft->update(['deleted_at' => now()]);
}, 3);
}
/** @return array<string,mixed> */
public function present(SimulationFormDraft $draft): array
{
return [
'id' => $draft->id,
'version_no' => $draft->version_no,
'snapshot_sha256' => $draft->snapshot_sha256,
'personal_snapshot' => SimulationFormDraft::decryptSnapshot($draft->personal_snapshot),
'economic_snapshot' => SimulationFormDraft::decryptSnapshot($draft->economic_snapshot),
'updated_at' => $draft->updated_at,
];
}
public function etag(SimulationFormDraft $draft): string
{
return '"sfd-'.$draft->id.'-v'.$draft->version_no.'"';
}
/** @return array{0:int|null,1:string|null} */
private function context(Request $request): array
{
$actorId = $request->user()?->id;
$token = $request->header('X-Simulation-Token');
abort_unless($actorId !== null || (is_string($token) && strlen($token) >= 32 && strlen($token) <= 256), 422,
'X-Simulation-Token opaco é obrigatório para um rascunho anónimo.');
return [$actorId, $actorId === null ? hash_hmac('sha256', $token, (string) config('app.key')) : null];
}
private function setContext(?int $actorId, ?string $tokenHash): void
{
DB::select("select set_config('app.actor_user_id', ?, true), set_config('app.simulation_token_hash', ?, true)", [(string) ($actorId ?? ''), (string) ($tokenHash ?? '')]);
}
private function requireMatch(Request $request, SimulationFormDraft $draft): void
{
$provided = $request->header('If-Match');
if (! $provided) {
throw new HttpException(428, 'If-Match é obrigatório.');
}
if (! hash_equals($this->etag($draft), trim($provided))) {
throw new HttpException(412, 'O rascunho foi alterado desde a última leitura.');
}
}
private function canonical(mixed $value): mixed
{
if (! is_array($value)) {
return $value;
}
if (! array_is_list($value)) {
ksort($value);
}
foreach ($value as $key => $item) {
$value[$key] = $this->canonical($item);
}
return $value;
}
}
+14
View File
@@ -8,6 +8,9 @@ use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
use Throwable;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
@@ -30,4 +33,15 @@ return Application::configure(basePath: dirname(__DIR__))
$exceptions->shouldRenderJsonWhen(
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
$exceptions->respond(function (Response $response, Throwable $exception, Request $request): Response {
if ($request->is('api/*')) {
$response->headers->set(
'X-Correlation-ID',
(string) ($request->attributes->get('correlation_id')
?? (Str::isUuid($request->header('X-Correlation-ID')) ? $request->header('X-Correlation-ID') : Str::uuid())),
);
}
return $response;
});
})->create();
+8
View File
@@ -4,6 +4,14 @@ return [
'compatibility_shadow' => (bool) env('INVESTMOZ_MNE_COMPATIBILITY_SHADOW', false),
'shadow_queue' => 'database',
'enabled' => (bool) env('INVESTMOZ_MNE_ENABLED', false),
'consent_policy_version' => env('INVESTMOZ_CONSENT_POLICY_VERSION', '2026-09-30'),
'sharing_policy_version' => env('INVESTMOZ_SHARING_POLICY_VERSION', '2026-09-30'),
'sla_escalation_enabled' => (bool) env('INVESTMOZ_SLA_ESCALATION_ENABLED', false),
// Designations do not establish legal competence or an institutional mandate.
'reviewers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_MNE_REVIEWERS', '')))))),
// These lists control application workflow access; they do not replace institutional delegation.
'triagers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_MNE_TRIAGERS', '')))))),
'approvers_l1' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_MNE_APPROVERS_L1', '')))))),
'approvers_l2' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_MNE_APPROVERS_L2', '')))))),
'publishers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_MNE_PUBLISHERS', '')))))),
];
@@ -0,0 +1,401 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const BUSINESS_STATUSES = [
'draft', 'submitted', 'in_review', 'needs_information', 'withdrawn',
'triage', 'information_pending', 'review', 'approval_l1', 'approval_l2',
'approved', 'published', 'suspended', 'rejected', 'cancelled', 'archived',
];
private const INTEREST_STATUSES = [
'received', 'qualification', 'pending', 'referred', 'contact_accepted',
'due_diligence', 'negotiation', 'refused', 'converted', 'cancelled', 'closed',
'in_review', 'reviewed', 'qualified', 'contacted', 'submitted',
];
public function up(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Marketplace extension requires PostgreSQL with PostGIS.');
}
Schema::table('existing_businesses', function (Blueprint $table) {
$table->string('public_title', 180)->nullable();
$table->string('public_slug', 220)->nullable();
$table->string('activity_code', 60)->nullable();
$table->string('stage', 40)->nullable();
$table->text('objective')->nullable();
$table->char('currency', 3)->nullable();
$table->decimal('amount_min', 20, 2)->nullable();
$table->decimal('amount_max', 20, 2)->nullable();
$table->decimal('available_percentage', 5, 2)->nullable();
$table->text('funds_use')->nullable();
$table->timestampTz('submitted_at')->nullable();
$table->timestampTz('published_at')->nullable();
$table->timestampTz('expires_at')->nullable();
$table->string('next_action', 180)->nullable();
$table->timestampTz('next_action_due_at')->nullable();
$table->foreignId('assigned_to')->nullable();
});
DB::statement('ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_assigned_to_fk FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL');
DB::statement(<<<'SQL'
DO $$
DECLARE constraint_row record;
BEGIN
FOR constraint_row IN
SELECT conname
FROM pg_constraint
WHERE conrelid = 'public.existing_businesses'::regclass
AND contype = 'c'
AND pg_get_constraintdef(oid) LIKE '%status%'
AND pg_get_constraintdef(oid) LIKE '%draft%'
LOOP
EXECUTE format('ALTER TABLE public.existing_businesses DROP CONSTRAINT %I', constraint_row.conname);
END LOOP;
END $$;
SQL);
DB::statement("ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_status_check CHECK (status IN ('".implode("','", self::BUSINESS_STATUSES)."'))");
DB::statement("ALTER TABLE existing_business_reviews DROP CONSTRAINT existing_business_reviews_action_check, ADD CONSTRAINT existing_business_reviews_action_check CHECK (action IN ('created','updated','archived','object_added','object_updated','verification','submitted','triage','in_review','needs_information','approval_l1','approval_l2','approved','published','rejected','withdrawn'))");
DB::statement('ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_amount_min_check CHECK (amount_min IS NULL OR amount_min >= 0)');
DB::statement('ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_amount_max_check CHECK (amount_max IS NULL OR amount_min IS NULL OR amount_max >= amount_min)');
DB::statement('ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_percentage_check CHECK (available_percentage IS NULL OR available_percentage BETWEEN 0 AND 100)');
DB::statement('ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_currency_check CHECK ((amount_min IS NULL AND amount_max IS NULL) OR currency IS NOT NULL)');
DB::statement("ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_published_check CHECK (status <> 'published' OR (published_at IS NOT NULL AND public_slug IS NOT NULL))");
DB::statement("ALTER TABLE existing_businesses ADD CONSTRAINT existing_businesses_public_slug_check CHECK (public_slug IS NULL OR public_slug ~ '^[a-z0-9]+(?:-[a-z0-9]+)*$')");
DB::statement('CREATE UNIQUE INDEX existing_businesses_public_slug_uq ON existing_businesses(public_slug) WHERE public_slug IS NOT NULL');
DB::statement('CREATE INDEX existing_businesses_queue_idx ON existing_businesses(status,assigned_to,next_action_due_at) WHERE deleted_at IS NULL');
Schema::table('investors', function (Blueprint $table) {
$table->jsonb('investment_sectors')->nullable();
$table->jsonb('investment_geographies')->nullable();
$table->text('investment_strategy')->nullable();
$table->unsignedInteger('investment_horizon_months')->nullable();
$table->decimal('ticket_min', 20, 2)->nullable();
$table->decimal('ticket_max', 20, 2)->nullable();
$table->char('ticket_currency', 3)->nullable();
});
DB::statement('ALTER TABLE investors ADD CONSTRAINT investors_ticket_range_check CHECK (ticket_max IS NULL OR ticket_min IS NULL OR ticket_max >= ticket_min)');
DB::statement('ALTER TABLE investors ADD CONSTRAINT investors_ticket_min_check CHECK (ticket_min IS NULL OR ticket_min >= 0)');
DB::statement('ALTER TABLE investors ADD CONSTRAINT investors_ticket_max_check CHECK (ticket_max IS NULL OR ticket_max >= 0)');
DB::statement('ALTER TABLE investors ADD CONSTRAINT investors_ticket_currency_check CHECK ((ticket_min IS NULL AND ticket_max IS NULL) OR ticket_currency IS NOT NULL)');
Schema::table('interest_requests', function (Blueprint $table) {
$table->foreignUuid('existing_business_id')->nullable();
$table->foreignUuid('investor_id')->nullable();
$table->string('modality', 50)->nullable();
$table->decimal('requested_percentage', 5, 2)->nullable();
$table->decimal('ticket_min', 20, 2)->nullable();
$table->decimal('ticket_max', 20, 2)->nullable();
$table->char('ticket_currency', 3)->nullable();
$table->foreignId('assigned_to')->nullable();
$table->string('next_action', 180)->nullable();
$table->timestampTz('next_action_due_at')->nullable();
});
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_existing_business_fk FOREIGN KEY (existing_business_id) REFERENCES existing_businesses(id) ON DELETE RESTRICT');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_investor_fk FOREIGN KEY (investor_id) REFERENCES investors(id) ON DELETE SET NULL');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_assigned_to_fk FOREIGN KEY (assigned_to) REFERENCES users(id) ON DELETE SET NULL');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_single_target_check CHECK (num_nonnulls(opportunity_id, existing_business_id) <= 1)');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_percentage_check CHECK (requested_percentage IS NULL OR requested_percentage BETWEEN 0 AND 100)');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_ticket_min_check CHECK (ticket_min IS NULL OR ticket_min >= 0)');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_ticket_max_check CHECK (ticket_max IS NULL OR ticket_max >= 0)');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_ticket_range_check CHECK (ticket_max IS NULL OR ticket_min IS NULL OR ticket_max >= ticket_min)');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_ticket_currency_check CHECK ((ticket_min IS NULL AND ticket_max IS NULL) OR ticket_currency IS NOT NULL)');
DB::statement("ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_status_check CHECK (status IN ('".implode("','", self::INTEREST_STATUSES)."'))");
Schema::table('interest_requests', fn (Blueprint $table) => $table->index(['existing_business_id', 'status'], 'interest_requests_business_status_idx'));
DB::statement('CREATE INDEX interest_requests_queue_idx ON interest_requests(status,assigned_to,next_action_due_at) WHERE deleted_at IS NULL');
DB::statement('ALTER TABLE interest_requests ADD CONSTRAINT interest_requests_business_modality_required_check CHECK (existing_business_id IS NULL OR modality IS NOT NULL)');
DB::statement("CREATE UNIQUE INDEX interest_business_email_modality_uq ON interest_requests(existing_business_id,lower(email),modality) WHERE existing_business_id IS NOT NULL AND deleted_at IS NULL AND status NOT IN ('refused','converted','cancelled','closed')");
Schema::table('consents', function (Blueprint $table) {
$table->foreignId('opportunity_id')->nullable();
$table->foreignUuid('existing_business_id')->nullable();
$table->foreignId('interest_request_id')->nullable();
$table->string('policy_version', 40)->nullable();
$table->char('technical_fingerprint_hash', 64)->nullable();
});
DB::statement('ALTER TABLE consents ADD CONSTRAINT consents_opportunity_fk FOREIGN KEY (opportunity_id) REFERENCES opportunities(id) ON DELETE SET NULL');
DB::statement('ALTER TABLE consents ADD CONSTRAINT consents_existing_business_fk FOREIGN KEY (existing_business_id) REFERENCES existing_businesses(id) ON DELETE RESTRICT');
DB::statement('ALTER TABLE consents ADD CONSTRAINT consents_interest_request_fk FOREIGN KEY (interest_request_id) REFERENCES interest_requests(id) ON DELETE RESTRICT');
DB::statement('ALTER TABLE consents ADD CONSTRAINT consents_subject_target_check CHECK (num_nonnulls(opportunity_id, existing_business_id, interest_request_id) <= 1)');
DB::statement(<<<'SQL'
ALTER TABLE consents ADD CONSTRAINT consents_fingerprint_check CHECK (technical_fingerprint_hash IS NULL OR technical_fingerprint_hash ~ '^[0-9a-f]{64}$')
SQL);
Schema::table('existing_business_objects', function (Blueprint $table) {
$table->string('asset_type', 50)->nullable();
$table->decimal('area', 18, 4)->nullable();
$table->string('area_unit', 20)->nullable();
$table->string('occupancy_status', 40)->nullable();
$table->string('condition_status', 40)->nullable();
});
DB::statement('ALTER TABLE existing_business_objects ADD CONSTRAINT existing_business_objects_area_check CHECK (area IS NULL OR area >= 0)');
Schema::table('documents', function (Blueprint $table) {
$table->unsignedBigInteger('size_bytes')->nullable();
$table->string('issuer', 180)->nullable();
$table->date('issued_on')->nullable();
$table->date('valid_until')->nullable();
$table->foreignId('uploaded_by')->nullable();
});
DB::statement('ALTER TABLE documents ADD CONSTRAINT documents_uploaded_by_fk FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL');
DB::statement('ALTER TABLE documents ADD CONSTRAINT documents_size_bytes_check CHECK (size_bytes IS NULL OR size_bytes > 0)');
DB::statement('ALTER TABLE documents ADD CONSTRAINT documents_valid_dates_check CHECK (valid_until IS NULL OR issued_on IS NULL OR valid_until >= issued_on)');
Schema::table('tasks', function (Blueprint $table) {
$table->foreignUuid('existing_business_id')->nullable()->constrained('existing_businesses')->restrictOnDelete();
$table->foreignId('interest_request_id')->nullable()->constrained('interest_requests')->restrictOnDelete();
$table->string('task_type', 60)->nullable();
$table->unsignedSmallInteger('priority')->default(3);
$table->timestampTz('completed_at')->nullable();
$table->string('outcome', 80)->nullable();
});
DB::statement('ALTER TABLE tasks ADD CONSTRAINT tasks_single_aggregate_check CHECK (num_nonnulls(opportunity_id, existing_business_id, interest_request_id) <= 1)');
DB::statement('ALTER TABLE tasks ADD CONSTRAINT tasks_priority_range_check CHECK (priority BETWEEN 1 AND 5)');
DB::statement('CREATE INDEX tasks_existing_business_idx ON tasks(existing_business_id,due_at) WHERE existing_business_id IS NOT NULL');
DB::statement('CREATE INDEX tasks_interest_request_idx ON tasks(interest_request_id,due_at) WHERE interest_request_id IS NOT NULL');
Schema::table('outbox_events', function (Blueprint $table) {
$table->string('idempotency_key', 120)->nullable()->unique();
$table->unsignedInteger('attempts')->default(0);
$table->timestampTz('failed_at')->nullable();
$table->text('last_error')->nullable();
});
$this->createBusinessLocations();
$this->createBusinessRights();
$this->createBusinessDocumentLinks();
$this->createWorkflowTransitions();
$this->createExternalRecordLinks();
}
public function down(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Marketplace extension rollback requires PostgreSQL.');
}
$this->refuseRollbackWithData();
DB::statement('DROP TRIGGER IF EXISTS existing_business_locations_hierarchy_trigger ON existing_business_locations');
DB::statement('DROP FUNCTION IF EXISTS enforce_existing_business_location_hierarchy()');
DB::statement('DROP TABLE IF EXISTS external_record_links RESTRICT');
DB::statement('DROP TABLE IF EXISTS business_workflow_transitions RESTRICT');
DB::statement('DROP TABLE IF EXISTS existing_business_document_links RESTRICT');
DB::statement('DROP TABLE IF EXISTS existing_business_rights RESTRICT');
DB::statement('DROP TABLE IF EXISTS existing_business_locations RESTRICT');
Schema::table('outbox_events', function (Blueprint $table) {
$table->dropUnique('outbox_events_idempotency_key_unique');
$table->dropColumn(['idempotency_key', 'attempts', 'failed_at', 'last_error']);
});
DB::statement('DROP INDEX tasks_existing_business_idx');
DB::statement('DROP INDEX tasks_interest_request_idx');
DB::statement('ALTER TABLE tasks DROP CONSTRAINT tasks_single_aggregate_check, DROP CONSTRAINT tasks_priority_range_check');
Schema::table('tasks', function (Blueprint $table) {
$table->dropForeign(['existing_business_id']);
$table->dropForeign(['interest_request_id']);
});
Schema::table('tasks', fn (Blueprint $table) => $table->dropColumn(['existing_business_id', 'interest_request_id', 'task_type', 'priority', 'completed_at', 'outcome']));
DB::statement('ALTER TABLE documents DROP CONSTRAINT documents_uploaded_by_fk, DROP CONSTRAINT documents_size_bytes_check, DROP CONSTRAINT documents_valid_dates_check');
Schema::table('documents', fn (Blueprint $table) => $table->dropColumn(['size_bytes', 'issuer', 'issued_on', 'valid_until', 'uploaded_by']));
DB::statement('ALTER TABLE existing_business_objects DROP CONSTRAINT existing_business_objects_area_check');
Schema::table('existing_business_objects', fn (Blueprint $table) => $table->dropColumn(['asset_type', 'area', 'area_unit', 'occupancy_status', 'condition_status']));
DB::statement('ALTER TABLE consents DROP CONSTRAINT consents_opportunity_fk, DROP CONSTRAINT consents_existing_business_fk, DROP CONSTRAINT consents_interest_request_fk, DROP CONSTRAINT consents_subject_target_check, DROP CONSTRAINT consents_fingerprint_check');
Schema::table('consents', fn (Blueprint $table) => $table->dropColumn(['opportunity_id', 'existing_business_id', 'interest_request_id', 'policy_version', 'technical_fingerprint_hash']));
Schema::table('interest_requests', fn (Blueprint $table) => $table->dropIndex('interest_requests_business_status_idx'));
DB::statement('DROP INDEX interest_requests_queue_idx');
DB::statement('DROP INDEX interest_business_email_modality_uq');
DB::statement('ALTER TABLE interest_requests DROP CONSTRAINT interest_requests_business_modality_required_check, DROP CONSTRAINT interest_requests_existing_business_fk, DROP CONSTRAINT interest_requests_investor_fk, DROP CONSTRAINT interest_requests_assigned_to_fk, DROP CONSTRAINT interest_requests_single_target_check, DROP CONSTRAINT interest_requests_percentage_check, DROP CONSTRAINT interest_requests_ticket_min_check, DROP CONSTRAINT interest_requests_ticket_max_check, DROP CONSTRAINT interest_requests_ticket_range_check, DROP CONSTRAINT interest_requests_ticket_currency_check, DROP CONSTRAINT interest_requests_status_check');
Schema::table('interest_requests', fn (Blueprint $table) => $table->dropColumn([
'existing_business_id', 'investor_id', 'modality', 'requested_percentage', 'ticket_min', 'ticket_max', 'ticket_currency', 'assigned_to', 'next_action', 'next_action_due_at',
]));
DB::statement('ALTER TABLE investors DROP CONSTRAINT investors_ticket_range_check, DROP CONSTRAINT investors_ticket_min_check, DROP CONSTRAINT investors_ticket_max_check, DROP CONSTRAINT investors_ticket_currency_check');
Schema::table('investors', fn (Blueprint $table) => $table->dropColumn([
'investment_sectors', 'investment_geographies', 'investment_strategy', 'investment_horizon_months', 'ticket_min', 'ticket_max', 'ticket_currency',
]));
DB::statement('ALTER TABLE existing_businesses DROP CONSTRAINT existing_businesses_assigned_to_fk, DROP CONSTRAINT existing_businesses_amount_min_check, DROP CONSTRAINT existing_businesses_amount_max_check, DROP CONSTRAINT existing_businesses_percentage_check, DROP CONSTRAINT existing_businesses_currency_check, DROP CONSTRAINT existing_businesses_published_check');
DB::statement('DROP INDEX existing_businesses_public_slug_uq');
DB::statement('ALTER TABLE existing_businesses DROP CONSTRAINT existing_businesses_public_slug_check');
DB::statement('DROP INDEX existing_businesses_queue_idx');
DB::statement("ALTER TABLE existing_business_reviews DROP CONSTRAINT existing_business_reviews_action_check, ADD CONSTRAINT existing_business_reviews_action_check CHECK (action IN ('submitted','in_review','needs_information','withdrawn'))");
DB::statement("ALTER TABLE existing_businesses DROP CONSTRAINT existing_businesses_status_check, ADD CONSTRAINT existing_businesses_status_check CHECK (status IN ('draft','submitted','in_review','needs_information','withdrawn'))");
Schema::table('existing_businesses', fn (Blueprint $table) => $table->dropColumn([
'public_title', 'public_slug', 'activity_code', 'stage', 'objective', 'currency', 'amount_min', 'amount_max', 'available_percentage', 'funds_use', 'submitted_at', 'published_at', 'expires_at', 'next_action', 'next_action_due_at', 'assigned_to',
]));
}
private function createBusinessLocations(): void
{
Schema::create('existing_business_locations', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('existing_business_id')->constrained('existing_businesses')->cascadeOnDelete();
$table->foreignUuid('province_id')->nullable()->constrained('geo_locations')->restrictOnDelete();
$table->foreignUuid('district_id')->nullable()->constrained('geo_locations')->restrictOnDelete();
$table->foreignUuid('admin_post_id')->nullable()->constrained('geo_locations')->restrictOnDelete();
$table->foreignUuid('locality_id')->nullable()->constrained('geo_locations')->restrictOnDelete();
$table->string('precision_level', 20)->default('district');
$table->boolean('is_primary')->default(false);
$table->boolean('is_public')->default(false);
$table->timestampsTz();
});
DB::statement('ALTER TABLE existing_business_locations ADD COLUMN geom geometry(Geometry,4326)');
DB::statement("ALTER TABLE existing_business_locations ADD CONSTRAINT existing_business_locations_precision_check CHECK (precision_level IN ('province','district','admin_post','locality','exact'))");
DB::statement('ALTER TABLE existing_business_locations ADD CONSTRAINT existing_business_locations_geom_check CHECK (geom IS NULL OR (ST_SRID(geom)=4326 AND NOT ST_IsEmpty(geom) AND ST_IsValid(geom)))');
DB::statement("ALTER TABLE existing_business_locations ADD CONSTRAINT existing_business_locations_precision_target_check CHECK ((precision_level <> 'province' OR province_id IS NOT NULL) AND (precision_level <> 'district' OR district_id IS NOT NULL) AND (precision_level <> 'admin_post' OR admin_post_id IS NOT NULL) AND (precision_level <> 'locality' OR locality_id IS NOT NULL) AND (precision_level <> 'exact' OR geom IS NOT NULL))");
DB::statement('CREATE UNIQUE INDEX existing_business_primary_location_uq ON existing_business_locations(existing_business_id) WHERE is_primary');
DB::statement('CREATE INDEX existing_business_locations_gist ON existing_business_locations USING gist(geom)');
DB::unprepared(<<<'SQL'
CREATE FUNCTION enforce_existing_business_location_hierarchy() RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE province_parent uuid; district_parent uuid; post_parent uuid;
BEGIN
IF NEW.province_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM geo_locations WHERE id=NEW.province_id AND location_type='province' AND deleted_at IS NULL) THEN RAISE EXCEPTION 'Invalid province reference'; END IF;
IF NEW.district_id IS NOT NULL THEN
SELECT parent_id INTO province_parent FROM geo_locations WHERE id=NEW.district_id AND location_type='district' AND deleted_at IS NULL;
IF province_parent IS NULL OR (NEW.province_id IS NOT NULL AND province_parent<>NEW.province_id) THEN RAISE EXCEPTION 'Invalid district/province relationship'; END IF;
END IF;
IF NEW.admin_post_id IS NOT NULL THEN
SELECT parent_id INTO district_parent FROM geo_locations WHERE id=NEW.admin_post_id AND location_type='admin_post' AND deleted_at IS NULL;
IF district_parent IS NULL OR (NEW.district_id IS NOT NULL AND district_parent<>NEW.district_id) THEN RAISE EXCEPTION 'Invalid administrative post/district relationship'; END IF;
END IF;
IF NEW.locality_id IS NOT NULL THEN
SELECT parent_id INTO post_parent FROM geo_locations WHERE id=NEW.locality_id AND location_type='locality' AND deleted_at IS NULL;
IF post_parent IS NULL OR (NEW.admin_post_id IS NOT NULL AND post_parent<>NEW.admin_post_id) THEN RAISE EXCEPTION 'Invalid locality/administrative post relationship'; END IF;
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER existing_business_locations_hierarchy_trigger BEFORE INSERT OR UPDATE OF province_id,district_id,admin_post_id,locality_id ON existing_business_locations FOR EACH ROW EXECUTE FUNCTION enforce_existing_business_location_hierarchy();
SQL);
}
private function createBusinessRights(): void
{
Schema::create('existing_business_rights', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('existing_business_id')->constrained('existing_businesses')->restrictOnDelete();
$table->foreignUuid('business_object_id')->nullable()->constrained('existing_business_objects')->restrictOnDelete();
$table->string('right_type', 50);
$table->string('reference', 120)->nullable();
$table->string('issuer', 180)->nullable();
$table->date('valid_from')->nullable();
$table->date('valid_until')->nullable();
$table->timestampTz('verified_at')->nullable();
$table->text('confidential_payload')->nullable();
$table->timestampsTz();
});
DB::statement('ALTER TABLE existing_business_rights ADD CONSTRAINT existing_business_rights_valid_dates_check CHECK (valid_until IS NULL OR valid_from IS NULL OR valid_until >= valid_from)');
DB::statement('CREATE INDEX existing_business_rights_business_idx ON existing_business_rights(existing_business_id)');
}
private function createBusinessDocumentLinks(): void
{
Schema::create('existing_business_document_links', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('existing_business_id')->nullable()->constrained('existing_businesses')->restrictOnDelete();
$table->foreignId('interest_request_id')->nullable()->constrained('interest_requests')->restrictOnDelete();
$table->foreignUuid('document_id')->constrained('documents')->restrictOnDelete();
$table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestampsTz();
$table->unique(['existing_business_id', 'document_id'], 'existing_business_document_uq');
$table->unique(['interest_request_id', 'document_id'], 'interest_request_document_uq');
});
DB::statement('ALTER TABLE existing_business_document_links ADD CONSTRAINT existing_business_document_target_check CHECK (num_nonnulls(existing_business_id, interest_request_id)=1)');
}
private function createWorkflowTransitions(): void
{
Schema::create('business_workflow_transitions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('existing_business_id')->nullable()->constrained('existing_businesses')->restrictOnDelete();
$table->foreignId('interest_request_id')->nullable()->constrained('interest_requests')->restrictOnDelete();
$table->string('from_status', 40)->nullable();
$table->string('to_status', 40);
$table->foreignId('actor_id')->nullable()->constrained('users')->nullOnDelete();
$table->text('reason')->nullable();
$table->uuid('correlation_id');
$table->jsonb('metadata')->default('{}');
$table->timestampTz('created_at')->useCurrent();
});
DB::statement('ALTER TABLE business_workflow_transitions ADD CONSTRAINT business_workflow_transition_target_check CHECK (num_nonnulls(existing_business_id, interest_request_id)=1)');
DB::statement('CREATE INDEX business_workflow_history_idx ON business_workflow_transitions(existing_business_id,created_at) WHERE existing_business_id IS NOT NULL');
DB::statement('CREATE INDEX interest_workflow_history_idx ON business_workflow_transitions(interest_request_id,created_at) WHERE interest_request_id IS NOT NULL');
}
private function createExternalRecordLinks(): void
{
Schema::create('external_record_links', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('existing_business_id')->nullable()->constrained('existing_businesses')->restrictOnDelete();
$table->foreignId('interest_request_id')->nullable()->constrained('interest_requests')->restrictOnDelete();
$table->foreignUuid('investor_id')->nullable()->constrained('investors')->restrictOnDelete();
$table->string('crm_system', 40);
$table->string('remote_id', 120);
$table->timestampTz('last_synced_at')->nullable();
$table->char('sync_hash', 64)->nullable();
$table->timestampsTz();
$table->unique(['crm_system', 'remote_id']);
});
DB::statement('ALTER TABLE external_record_links ADD CONSTRAINT external_record_target_check CHECK (num_nonnulls(existing_business_id, interest_request_id, investor_id)=1)');
DB::statement('CREATE UNIQUE INDEX external_record_business_crm_uq ON external_record_links(existing_business_id,crm_system) WHERE existing_business_id IS NOT NULL');
DB::statement('CREATE UNIQUE INDEX external_record_interest_crm_uq ON external_record_links(interest_request_id,crm_system) WHERE interest_request_id IS NOT NULL');
DB::statement('CREATE UNIQUE INDEX external_record_investor_crm_uq ON external_record_links(investor_id,crm_system) WHERE investor_id IS NOT NULL');
DB::statement(<<<'SQL'
ALTER TABLE external_record_links ADD CONSTRAINT external_record_sync_hash_check CHECK (sync_hash IS NULL OR sync_hash ~ '^[0-9a-f]{64}$')
SQL);
}
private function refuseRollbackWithData(): void
{
foreach (['existing_business_locations', 'existing_business_rights', 'existing_business_document_links', 'business_workflow_transitions', 'external_record_links'] as $table) {
if (DB::table($table)->exists()) {
throw new RuntimeException('Marketplace extension contains data; reconcile records before rollback.');
}
}
if (DB::table('existing_businesses')->whereIn('status', array_diff(self::BUSINESS_STATUSES, ['draft', 'submitted', 'in_review', 'needs_information', 'withdrawn']))->exists()
|| DB::table('existing_businesses')->where(function ($query) {
$query->whereNotNull('public_title')->orWhereNotNull('public_slug')->orWhereNotNull('activity_code')->orWhereNotNull('stage')->orWhereNotNull('objective')
->orWhereNotNull('currency')->orWhereNotNull('amount_min')->orWhereNotNull('amount_max')->orWhereNotNull('available_percentage')
->orWhereNotNull('funds_use')->orWhereNotNull('submitted_at')->orWhereNotNull('published_at')->orWhereNotNull('expires_at')
->orWhereNotNull('next_action')->orWhereNotNull('next_action_due_at')->orWhereNotNull('assigned_to');
})->exists()
|| DB::table('interest_requests')->where(function ($query) {
$query->whereNotNull('existing_business_id')->orWhereNotNull('investor_id')->orWhereNotNull('modality')
->orWhereNotNull('requested_percentage')->orWhereNotNull('ticket_min')->orWhereNotNull('ticket_max')
->orWhereNotNull('ticket_currency')->orWhereNotNull('assigned_to')->orWhereNotNull('next_action')->orWhereNotNull('next_action_due_at');
})->exists()
|| DB::table('documents')->where(function ($query) {
$query->whereNotNull('size_bytes')->orWhereNotNull('issuer')->orWhereNotNull('issued_on')->orWhereNotNull('valid_until')->orWhereNotNull('uploaded_by');
})->exists()
|| DB::table('tasks')->where(function ($query) {
$query->whereNotNull('existing_business_id')->orWhereNotNull('interest_request_id')->orWhereNotNull('task_type')
->orWhere('priority', '<>', 3)->orWhereNotNull('completed_at')->orWhereNotNull('outcome');
})->exists()
|| DB::table('outbox_events')->where(function ($query) {
$query->whereNotNull('idempotency_key')->orWhere('attempts', '<>', 0)->orWhereNotNull('failed_at')->orWhereNotNull('last_error');
})->exists()
|| DB::table('investors')->where(function ($query) {
$query->whereNotNull('investment_sectors')->orWhereNotNull('investment_geographies')->orWhereNotNull('investment_strategy')
->orWhereNotNull('investment_horizon_months')->orWhereNotNull('ticket_min')->orWhereNotNull('ticket_max')->orWhereNotNull('ticket_currency');
})->exists()
|| DB::table('existing_business_objects')->where(function ($query) {
$query->whereNotNull('asset_type')->orWhereNotNull('area')->orWhereNotNull('area_unit')->orWhereNotNull('occupancy_status')->orWhereNotNull('condition_status');
})->exists()
|| DB::table('consents')->where(function ($query) {
$query->whereNotNull('opportunity_id')->orWhereNotNull('existing_business_id')->orWhereNotNull('interest_request_id')
->orWhereNotNull('policy_version')->orWhereNotNull('technical_fingerprint_hash');
})->exists()) {
throw new RuntimeException('Marketplace extension fields contain data; reconcile records before rollback.');
}
}
};
@@ -0,0 +1,131 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Reference catalogs require PostgreSQL.');
}
Schema::create('reference_catalog_versions', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('catalog_code', 60);
$table->unsignedInteger('version_no');
$table->string('locale', 10)->default('pt-MZ');
$table->string('source_system', 100);
$table->string('source_version', 100);
$table->text('source_reference');
$table->char('source_sha256', 64)->nullable();
$table->date('valid_from')->nullable();
$table->date('valid_to')->nullable();
$table->string('status', 16)->default('draft');
$table->timestampsTz();
$table->unique(['catalog_code', 'version_no', 'locale'], 'reference_catalog_version_uq');
$table->index(['catalog_code', 'status', 'valid_from', 'valid_to'], 'reference_catalog_current_idx');
});
DB::statement("ALTER TABLE reference_catalog_versions ADD CONSTRAINT reference_catalog_version_check CHECK (version_no > 0 AND (valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from) AND status IN ('draft','published','retired') AND (source_sha256 IS NULL OR source_sha256 ~ '^[0-9a-f]{64}$'))");
DB::statement("CREATE UNIQUE INDEX reference_catalog_one_published_uq ON reference_catalog_versions(catalog_code, locale) WHERE status='published'");
Schema::create('authorities', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('catalog_version_id')->constrained('reference_catalog_versions')->restrictOnDelete();
$table->foreignUuid('organization_id')->constrained('organizations')->restrictOnDelete();
$table->foreignUuid('jurisdiction_id')->nullable()->constrained('geo_locations')->restrictOnDelete();
$table->string('code', 100);
$table->string('authority_type', 60);
$table->string('label_pt_mz', 255);
$table->text('source_reference');
$table->unsignedInteger('version_no')->default(1);
$table->date('valid_from');
$table->date('valid_to')->nullable();
$table->string('status', 16)->default('draft');
$table->timestampsTz();
$table->unique(['catalog_version_id', 'code'], 'authorities_catalog_code_uq');
$table->index(['jurisdiction_id', 'status', 'valid_from', 'valid_to'], 'authorities_jurisdiction_status_idx');
$table->index(['authority_type', 'status'], 'authorities_type_status_idx');
});
DB::statement('ALTER TABLE authorities ADD CONSTRAINT authorities_check CHECK (version_no > 0 AND (valid_to IS NULL OR valid_to > valid_from))');
DB::statement("ALTER TABLE authorities ADD CONSTRAINT authorities_status_check CHECK (status IN ('draft','published','retired'))");
Schema::create('economic_activities', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->foreignUuid('catalog_version_id')->constrained('reference_catalog_versions')->restrictOnDelete();
$table->uuid('parent_id')->nullable();
$table->foreignId('sector_id')->constrained('sectors')->restrictOnDelete();
$table->string('code', 100);
$table->string('label_pt_mz', 255);
$table->text('description_pt_mz')->nullable();
$table->unsignedInteger('version_no')->default(1);
$table->date('valid_from');
$table->date('valid_to')->nullable();
$table->string('status', 16)->default('published');
$table->timestampsTz();
$table->unique(['catalog_version_id', 'code'], 'economic_activities_catalog_code_uq');
$table->unique(['catalog_version_id', 'id'], 'economic_activities_version_id_uq');
$table->unique('sector_id', 'economic_activities_sector_uq');
$table->index(['parent_id', 'status', 'label_pt_mz'], 'economic_activities_parent_status_idx');
$table->index(['status', 'valid_from', 'valid_to'], 'economic_activities_validity_idx');
});
DB::statement('ALTER TABLE economic_activities ADD CONSTRAINT economic_activities_parent_same_version_fk FOREIGN KEY (catalog_version_id, parent_id) REFERENCES economic_activities(catalog_version_id, id) ON DELETE RESTRICT');
DB::statement('ALTER TABLE economic_activities ADD CONSTRAINT economic_activities_check CHECK (version_no > 0 AND (valid_to IS NULL OR valid_to > valid_from))');
DB::statement("ALTER TABLE economic_activities ADD CONSTRAINT economic_activities_status_check CHECK (status IN ('draft','published','retired'))");
DB::unprepared(<<<'SQL'
CREATE OR REPLACE FUNCTION public.investmoz_guard_reference_catalog_mutation() RETURNS trigger
LANGUAGE plpgsql SET search_path = pg_catalog, public AS $$
BEGIN
IF TG_OP = 'DELETE' AND OLD.status = 'published' THEN
RAISE EXCEPTION 'Published reference catalog entries are immutable' USING ERRCODE = '42501';
END IF;
IF TG_OP = 'UPDATE' AND OLD.status = 'published' THEN
IF NEW.status <> 'retired'
OR NEW.valid_to IS NULL
OR (to_jsonb(NEW) - ARRAY['status','valid_to','updated_at'])
IS DISTINCT FROM
(to_jsonb(OLD) - ARRAY['status','valid_to','updated_at']) THEN
RAISE EXCEPTION 'Published reference catalog entries are immutable; publish a new version' USING ERRCODE = '42501';
END IF;
END IF;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER reference_catalog_version_immutable BEFORE UPDATE OR DELETE ON public.reference_catalog_versions
FOR EACH ROW EXECUTE FUNCTION public.investmoz_guard_reference_catalog_mutation();
CREATE TRIGGER authority_catalog_entry_immutable BEFORE UPDATE OR DELETE ON public.authorities
FOR EACH ROW EXECUTE FUNCTION public.investmoz_guard_reference_catalog_mutation();
CREATE TRIGGER economic_activity_catalog_entry_immutable BEFORE UPDATE OR DELETE ON public.economic_activities
FOR EACH ROW EXECUTE FUNCTION public.investmoz_guard_reference_catalog_mutation();
SQL);
}
public function down(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Reference catalog rollback requires PostgreSQL.');
}
if (Schema::hasTable('authorities') && DB::table('authorities')->exists()) {
throw new RuntimeException('Authority references exist; preserve them before rolling back reference catalogs.');
}
if (Schema::hasTable('economic_activities') && DB::table('economic_activities')->exists()) {
throw new RuntimeException('Economic activities exist; preserve them before rolling back reference catalogs.');
}
Schema::dropIfExists('economic_activities');
Schema::dropIfExists('authorities');
Schema::dropIfExists('reference_catalog_versions');
DB::statement('DROP FUNCTION IF EXISTS public.investmoz_guard_reference_catalog_mutation() RESTRICT');
}
};
@@ -0,0 +1,220 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
private const TABLES = [
'simulation_audit_events', 'simulation_idempotency', 'simulation_jobs', 'simulation_reports',
'simulation_obligations', 'simulation_result_sources', 'simulation_results', 'simulation_scenarios',
'simulations', 'questionnaire_versions',
];
public function up(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Simulation execution requires PostgreSQL.');
}
DB::unprepared(<<<'SQL'
CREATE TABLE public.questionnaire_versions (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code text NOT NULL,
version_no integer NOT NULL CHECK (version_no > 0),
locale text NOT NULL DEFAULT 'pt-MZ',
schema_snapshot jsonb NOT NULL,
status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','published','retired')),
valid_from date,
valid_to date,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (code, version_no, locale),
CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to > valid_from)
);
INSERT INTO public.questionnaire_versions (id, code, version_no, locale, schema_snapshot, status, valid_from)
VALUES ('61000100-0000-4000-8000-000000000001', 'user-entered-arithmetic', 1, 'pt-MZ',
'{"inputs":{"base":{"type":"decimal","minimum":0},"rate":{"type":"decimal","minimum":0,"maximum":100},"reduction":{"type":"decimal","minimum":0,"maximum":100,"default":0}},"rate_origin":"user_input","currency":null}'::jsonb,
'published', DATE '2026-10-01');
CREATE TABLE public.simulations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
questionnaire_version_id uuid REFERENCES public.questionnaire_versions(id) ON DELETE RESTRICT,
owner_user_id bigint REFERENCES public.users(id) ON DELETE RESTRICT,
organization_id uuid REFERENCES public.organizations(id) ON DELETE RESTRICT,
anonymous_token_hash char(64),
version_no integer NOT NULL DEFAULT 1 CHECK (version_no > 0),
status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','running','completed','failed','cancelled')),
input_snapshot jsonb NOT NULL,
result_snapshot jsonb,
calculation_snapshot jsonb,
error_code text,
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK ((owner_user_id IS NOT NULL)::integer + (anonymous_token_hash IS NOT NULL)::integer = 1),
CHECK (anonymous_token_hash IS NULL OR anonymous_token_hash ~ '^[0-9a-f]{64}$')
);
CREATE INDEX simulations_owner_created_idx ON public.simulations(owner_user_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE INDEX simulations_org_created_idx ON public.simulations(organization_id, created_at DESC) WHERE deleted_at IS NULL;
CREATE TABLE public.simulation_scenarios (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
simulation_id uuid NOT NULL REFERENCES public.simulations(id) ON DELETE CASCADE,
code text NOT NULL,
label text NOT NULL,
version_no integer NOT NULL DEFAULT 1 CHECK (version_no > 0),
parameters jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (simulation_id, code)
);
CREATE TABLE public.simulation_results (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
simulation_id uuid NOT NULL REFERENCES public.simulations(id) ON DELETE CASCADE,
scenario_id uuid NOT NULL REFERENCES public.simulation_scenarios(id) ON DELETE RESTRICT,
engine_version text NOT NULL,
result_snapshot jsonb NOT NULL,
result_sha256 char(64) NOT NULL CHECK (result_sha256 ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (scenario_id, engine_version)
);
CREATE TABLE public.simulation_result_sources (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
result_id uuid NOT NULL REFERENCES public.simulation_results(id) ON DELETE CASCADE,
source_type text NOT NULL,
source_id text NOT NULL,
source_version text NOT NULL,
source_snapshot jsonb NOT NULL,
source_sha256 char(64) NOT NULL CHECK (source_sha256 ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (result_id, source_type, source_id, source_version)
);
CREATE TABLE public.simulation_obligations (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
simulation_id uuid NOT NULL REFERENCES public.simulations(id) ON DELETE CASCADE,
scenario_id uuid REFERENCES public.simulation_scenarios(id) ON DELETE RESTRICT,
code text NOT NULL,
label text NOT NULL,
source_reference text,
source_version text,
status text NOT NULL DEFAULT 'indicative' CHECK (status IN ('indicative','confirmed','dismissed')),
snapshot jsonb NOT NULL DEFAULT '{}'::jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (simulation_id, code)
);
CREATE TABLE public.simulation_reports (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
simulation_id uuid NOT NULL REFERENCES public.simulations(id) ON DELETE CASCADE,
version_no integer NOT NULL DEFAULT 1 CHECK (version_no > 0),
status text NOT NULL DEFAULT 'queued' CHECK (status IN ('queued','ready','failed')),
report_snapshot jsonb,
report_sha256 char(64),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK (report_sha256 IS NULL OR report_sha256 ~ '^[0-9a-f]{64}$'),
UNIQUE (simulation_id, version_no)
);
CREATE TABLE public.simulation_jobs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
simulation_id uuid NOT NULL REFERENCES public.simulations(id) ON DELETE CASCADE,
job_type text NOT NULL CHECK (job_type IN ('execute','report')),
status text NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','succeeded','failed')),
attempts smallint NOT NULL DEFAULT 0 CHECK (attempts BETWEEN 0 AND 3),
available_at timestamptz NOT NULL DEFAULT now(),
started_at timestamptz,
finished_at timestamptz,
error_code text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (simulation_id, job_type)
);
CREATE TABLE public.simulation_idempotency (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
scope_hash char(64) NOT NULL CHECK (scope_hash ~ '^[0-9a-f]{64}$'),
key_hash char(64) NOT NULL CHECK (key_hash ~ '^[0-9a-f]{64}$'),
payload_hash char(64) NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
simulation_id uuid REFERENCES public.simulations(id) ON DELETE RESTRICT,
response_snapshot jsonb,
created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE (scope_hash, key_hash)
);
CREATE TABLE public.simulation_audit_events (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
simulation_id uuid NOT NULL REFERENCES public.simulations(id) ON DELETE RESTRICT,
actor_user_id bigint REFERENCES public.users(id) ON DELETE RESTRICT,
action text NOT NULL,
correlation_id uuid NOT NULL,
data jsonb NOT NULL DEFAULT '{}'::jsonb,
occurred_at timestamptz NOT NULL DEFAULT now()
);
CREATE OR REPLACE FUNCTION public.investmoz_reject_simulation_audit_change() RETURNS trigger
LANGUAGE plpgsql SET search_path = pg_catalog, public AS $$
BEGIN RAISE EXCEPTION 'Simulation audit is append-only' USING ERRCODE = '42501'; END;
$$;
CREATE TRIGGER simulation_audit_immutable BEFORE UPDATE OR DELETE ON public.simulation_audit_events FOR EACH ROW EXECUTE FUNCTION public.investmoz_reject_simulation_audit_change();
CREATE OR REPLACE FUNCTION public.investmoz_reject_simulation_snapshot_change() RETURNS trigger
LANGUAGE plpgsql SET search_path = pg_catalog, public AS $$
BEGIN RAISE EXCEPTION 'Published simulation snapshots are immutable' USING ERRCODE = '42501'; END;
$$;
CREATE TRIGGER questionnaire_version_immutable BEFORE UPDATE OR DELETE ON public.questionnaire_versions FOR EACH ROW
WHEN (OLD.status = 'published') EXECUTE FUNCTION public.investmoz_reject_simulation_snapshot_change();
CREATE TRIGGER simulation_result_immutable BEFORE UPDATE OR DELETE ON public.simulation_results FOR EACH ROW EXECUTE FUNCTION public.investmoz_reject_simulation_snapshot_change();
CREATE TRIGGER simulation_result_source_immutable BEFORE UPDATE OR DELETE ON public.simulation_result_sources FOR EACH ROW EXECUTE FUNCTION public.investmoz_reject_simulation_snapshot_change();
CREATE TRIGGER simulation_report_immutable BEFORE UPDATE OR DELETE ON public.simulation_reports FOR EACH ROW
WHEN (OLD.status = 'ready') EXECUTE FUNCTION public.investmoz_reject_simulation_snapshot_change();
SQL);
$tables = array_diff(self::TABLES, ['simulations', 'simulation_audit_events', 'simulation_idempotency', 'questionnaire_versions']);
foreach ($tables as $table) {
$simulationId = $table === 'simulation_result_sources'
? '(SELECT parent.simulation_id FROM public.simulation_results parent WHERE parent.id = result_id)'
: ($table === 'simulations' ? 'id' : 'simulation_id');
$readAccess = "EXISTS (SELECT 1 FROM public.simulations s WHERE s.id = {$simulationId} AND ((s.organization_id IS NULL AND s.owner_user_id IS NOT NULL AND s.owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint) OR (s.organization_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.memberships m WHERE m.organization_id = s.organization_id AND m.user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint AND m.deleted_at IS NULL)) OR (s.anonymous_token_hash IS NOT NULL AND s.anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), ''))))";
$writeAccess = "EXISTS (SELECT 1 FROM public.simulations s WHERE s.id = {$simulationId} AND ((s.organization_id IS NULL AND s.owner_user_id IS NOT NULL AND s.owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint) OR (s.organization_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.memberships m WHERE m.organization_id = s.organization_id AND m.user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint AND m.deleted_at IS NULL AND m.role IN ('admin','manager','officer'))) OR (s.anonymous_token_hash IS NOT NULL AND s.anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), ''))) )";
DB::statement("ALTER TABLE public.{$table} ENABLE ROW LEVEL SECURITY");
DB::statement("ALTER TABLE public.{$table} FORCE ROW LEVEL SECURITY");
DB::statement("CREATE POLICY {$table}_read_policy ON public.{$table} FOR SELECT USING ({$readAccess})");
DB::statement("CREATE POLICY {$table}_write_policy ON public.{$table} FOR ALL USING ({$writeAccess}) WITH CHECK ({$writeAccess})");
}
foreach (['simulation_idempotency', 'simulation_audit_events'] as $table) {
DB::statement("ALTER TABLE public.{$table} ENABLE ROW LEVEL SECURITY");
DB::statement("ALTER TABLE public.{$table} FORCE ROW LEVEL SECURITY");
}
DB::statement('ALTER TABLE public.simulations ENABLE ROW LEVEL SECURITY');
DB::statement('ALTER TABLE public.simulations FORCE ROW LEVEL SECURITY');
DB::statement("CREATE POLICY simulations_read_policy ON public.simulations FOR SELECT USING (((organization_id IS NULL AND owner_user_id IS NOT NULL AND owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint) OR (organization_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.memberships m WHERE m.organization_id = simulations.organization_id AND m.user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint AND m.deleted_at IS NULL)) OR (anonymous_token_hash IS NOT NULL AND anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), ''))) AND deleted_at IS NULL)");
$simulationWrite = "((organization_id IS NULL AND owner_user_id IS NOT NULL AND owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint) OR (organization_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.memberships m WHERE m.organization_id = simulations.organization_id AND m.user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint AND m.deleted_at IS NULL AND m.role IN ('admin','manager','officer'))) OR (anonymous_token_hash IS NOT NULL AND anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), '')))";
DB::statement("CREATE POLICY simulations_write_policy ON public.simulations FOR ALL USING ({$simulationWrite}) WITH CHECK ({$simulationWrite})");
DB::statement('ALTER TABLE public.questionnaire_versions ENABLE ROW LEVEL SECURITY');
DB::statement('ALTER TABLE public.questionnaire_versions FORCE ROW LEVEL SECURITY');
DB::statement("CREATE POLICY questionnaire_published_read ON public.questionnaire_versions FOR SELECT USING (status = 'published')");
DB::statement("CREATE POLICY simulation_idempotency_actor ON public.simulation_idempotency USING (scope_hash = encode(digest(COALESCE(NULLIF(current_setting('app.actor_user_id', true), ''), NULLIF(current_setting('app.simulation_token_hash', true), ''), 'anonymous'), 'sha256'), 'hex')) WITH CHECK (scope_hash = encode(digest(COALESCE(NULLIF(current_setting('app.actor_user_id', true), ''), NULLIF(current_setting('app.simulation_token_hash', true), ''), 'anonymous'), 'sha256'), 'hex'))");
DB::statement("CREATE POLICY simulation_audit_read ON public.simulation_audit_events FOR SELECT USING (EXISTS (SELECT 1 FROM public.simulations s WHERE s.id = simulation_id AND s.owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint))");
DB::statement("CREATE POLICY simulation_audit_insert ON public.simulation_audit_events FOR INSERT WITH CHECK (actor_user_id IS NOT DISTINCT FROM NULLIF(current_setting('app.actor_user_id', true), '')::bigint AND EXISTS (SELECT 1 FROM public.simulations s WHERE s.id = simulation_id AND ((s.organization_id IS NULL AND s.owner_user_id IS NOT NULL AND s.owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint) OR (s.organization_id IS NOT NULL AND EXISTS (SELECT 1 FROM public.memberships m WHERE m.organization_id = s.organization_id AND m.user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint AND m.deleted_at IS NULL AND m.role IN ('admin','manager','officer'))) OR (s.anonymous_token_hash IS NOT NULL AND s.anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), '')))))");
}
public function down(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Simulation execution rollback requires PostgreSQL.');
}
foreach (self::TABLES as $table) {
if (Schema::hasTable($table)) {
DB::statement("ALTER TABLE public.{$table} DISABLE ROW LEVEL SECURITY");
$query = DB::table($table);
if ($table === 'questionnaire_versions') {
$query->where('id', '<>', '61000100-0000-4000-8000-000000000001');
}
if ($query->exists()) {
throw new RuntimeException("Cannot rollback simulation execution: {$table} contains data.");
}
}
}
foreach (self::TABLES as $table) {
Schema::dropIfExists($table);
}
DB::statement('DROP FUNCTION IF EXISTS public.investmoz_reject_simulation_audit_change() RESTRICT');
DB::statement('DROP FUNCTION IF EXISTS public.investmoz_reject_simulation_snapshot_change() RESTRICT');
}
};
@@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Simulation form drafts require PostgreSQL.');
}
DB::unprepared(<<<'SQL'
CREATE TABLE public.simulation_form_drafts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
owner_user_id bigint REFERENCES public.users(id) ON DELETE RESTRICT,
anonymous_token_hash char(64),
version_no integer NOT NULL DEFAULT 1 CHECK (version_no > 0),
create_key_hash char(64) NOT NULL CHECK (create_key_hash ~ '^[0-9a-f]{64}$'),
create_payload_hash char(64) NOT NULL CHECK (create_payload_hash ~ '^[0-9a-f]{64}$'),
snapshot_sha256 char(64) NOT NULL CHECK (snapshot_sha256 ~ '^[0-9a-f]{64}$'),
personal_snapshot text NOT NULL,
economic_snapshot text NOT NULL,
deleted_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CHECK ((owner_user_id IS NOT NULL)::integer + (anonymous_token_hash IS NOT NULL)::integer = 1),
CHECK (anonymous_token_hash IS NULL OR anonymous_token_hash ~ '^[0-9a-f]{64}$')
);
CREATE UNIQUE INDEX simulation_form_drafts_owner_active_unique ON public.simulation_form_drafts(owner_user_id)
WHERE owner_user_id IS NOT NULL AND deleted_at IS NULL;
CREATE UNIQUE INDEX simulation_form_drafts_token_active_unique ON public.simulation_form_drafts(anonymous_token_hash)
WHERE anonymous_token_hash IS NOT NULL AND deleted_at IS NULL;
ALTER TABLE public.simulation_form_drafts ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.simulation_form_drafts FORCE ROW LEVEL SECURITY;
CREATE POLICY simulation_form_drafts_read ON public.simulation_form_drafts FOR SELECT
USING (deleted_at IS NULL AND ((owner_user_id IS NOT NULL AND owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint)
OR (anonymous_token_hash IS NOT NULL AND anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), ''))));
CREATE POLICY simulation_form_drafts_write ON public.simulation_form_drafts FOR ALL
USING (deleted_at IS NULL AND ((owner_user_id IS NOT NULL AND owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint)
OR (anonymous_token_hash IS NOT NULL AND anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), ''))))
WITH CHECK ((owner_user_id IS NOT NULL AND owner_user_id = NULLIF(current_setting('app.actor_user_id', true), '')::bigint)
OR (anonymous_token_hash IS NOT NULL AND anonymous_token_hash = NULLIF(current_setting('app.simulation_token_hash', true), '')));
SQL);
}
public function down(): void
{
if (DB::connection()->getDriverName() !== 'pgsql') {
throw new RuntimeException('Simulation form draft rollback requires PostgreSQL.');
}
DB::statement('ALTER TABLE public.simulation_form_drafts DISABLE ROW LEVEL SECURITY');
if (DB::table('simulation_form_drafts')->exists()) {
throw new RuntimeException('Cannot rollback simulation form drafts while draft data exists.');
}
Schema::dropIfExists('simulation_form_drafts');
}
};
+1 -13
View File
@@ -2,11 +2,8 @@
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class DatabaseSeeder extends Seeder
{
@@ -24,15 +21,6 @@ class DatabaseSeeder extends Seeder
}
$this->call(InvestMozReferenceSeeder::class);
// User::factory(10)->create();
User::firstOrCreate(
['email' => 'test@example.com'],
[
'name' => 'Test User',
'password' => Hash::make(Str::random(48)),
],
);
$this->call(LocalTestUserSeeder::class);
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
class LocalTestUserSeeder extends Seeder
{
/**
* Create or reset the documented local development account.
*/
public function run(): void
{
if (! app()->environment(['local', 'testing'])) {
return;
}
$user = User::firstOrNew(['email' => 'test@example.com']);
$user->forceFill([
'name' => 'Test User',
'password' => 'password',
'email_verified_at' => now(),
'two_factor_secret' => null,
'two_factor_recovery_codes' => null,
'two_factor_confirmed_at' => null,
])->save();
}
}
@@ -14,6 +14,7 @@ class ProductionReferenceSeeder extends Seeder
InvestmentAreaSeeder::class,
InvestmentReferenceSeeder::class,
TerritoryReferenceSeeder::class,
ReferenceCatalogSeeder::class,
InvestmentPortfolioSeeder::class,
]);
}
+128
View File
@@ -0,0 +1,128 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use RuntimeException;
class ReferenceCatalogSeeder extends Seeder
{
private const CATALOG_IDS = [
'authorities' => 'a1000000-0000-4000-8000-000000000001',
'jurisdictions' => 'a1000000-0000-4000-8000-000000000002',
'economic-activities' => 'a1000000-0000-4000-8000-000000000003',
];
/** @var array<string,string> */
private const ACTIVITY_IDS = [
'agronegocio' => 'a2000000-0000-4000-8000-000000000001',
'energia' => 'a2000000-0000-4000-8000-000000000002',
'turismo' => 'a2000000-0000-4000-8000-000000000003',
'infraestruturas' => 'a2000000-0000-4000-8000-000000000004',
'economia-azul' => 'a2000000-0000-4000-8000-000000000005',
'mercado-financeiro' => 'a2000000-0000-4000-8000-000000000006',
];
public function run(): void
{
$territory = json_decode(File::get(database_path('data/territory-locations.json')), true, 512, JSON_THROW_ON_ERROR);
if (! is_array($territory) || ! is_array($territory['records'] ?? null) || ! is_int($territory['source_version'] ?? null)) {
throw new RuntimeException('Territorial catalog source is malformed.');
}
if (! is_string($territory['records_sha256'] ?? null)
|| ! hash_equals($territory['records_sha256'], hash('sha256', json_encode($territory['records'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)))
|| DB::table('geo_locations')->where('source_system', 'cartografia-workbook')->where('data_classification', 'public')->count() !== count($territory['records'])) {
throw new RuntimeException('Territorial catalog version does not match its verified seed records.');
}
DB::transaction(function () use ($territory): void {
DB::select('SELECT pg_advisory_xact_lock(20261001, 300100)');
$this->catalogVersion('authorities', 1, 'authority-register-pending-source', 'pending-authority-source',
'Awaiting an approved authority source; no institution or mandate is inferred.', null, null, 'draft');
$this->catalogVersion('jurisdictions', $territory['source_version'], 'cartografia-workbook', (string) $territory['source_version'],
'database/data/territory-locations.json', $territory['source_sha256'], null, 'published');
$activities = DB::table('sectors')->whereIn('slug', array_keys(self::ACTIVITY_IDS))->orderBy('slug')->get(['id', 'name', 'slug']);
if ($activities->count() !== count(self::ACTIVITY_IDS)) {
throw new RuntimeException('The curated sector source is incomplete; economic activity seed aborted.');
}
$canonical = $activities->map(fn ($row) => ['code' => $row->slug, 'label_pt_mz' => $row->name])->values()->all();
$activityChecksum = hash('sha256', json_encode($canonical, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR));
$versionId = $this->catalogVersion('economic-activities', 1, 'investmoz-curated', 'sector-seeder-v1',
'database/seeders/SectorSeeder.php; seed is a portal grouping, not an official activity classification.',
$activityChecksum, '2026-10-01', 'published');
foreach ($activities as $activity) {
$expected = [
'id' => self::ACTIVITY_IDS[$activity->slug],
'catalog_version_id' => $versionId,
'parent_id' => null,
'sector_id' => $activity->id,
'code' => $activity->slug,
'label_pt_mz' => $activity->name,
'description_pt_mz' => null,
'version_no' => 1,
'valid_from' => '2026-10-01',
'valid_to' => null,
'status' => 'published',
];
$existing = DB::table('economic_activities')->where('catalog_version_id', $versionId)->where('code', $activity->slug)->first();
if ($existing) {
foreach ($expected as $column => $value) {
if ((string) $existing->{$column} !== (string) $value) {
throw new RuntimeException('Economic activity differs from its versioned seed: '.$activity->slug);
}
}
continue;
}
DB::table('economic_activities')->insert([...$expected, 'created_at' => now(), 'updated_at' => now()]);
}
});
}
private function catalogVersion(
string $code,
int $version,
string $source,
string $sourceVersion,
string $reference,
?string $checksum,
?string $validFrom,
string $status,
): string {
$id = self::CATALOG_IDS[$code];
$expected = [
'id' => $id,
'catalog_code' => $code,
'version_no' => $version,
'locale' => 'pt-MZ',
'source_system' => $source,
'source_version' => $sourceVersion,
'source_reference' => $reference,
'source_sha256' => $checksum,
'valid_from' => $validFrom,
'valid_to' => null,
'status' => $status,
];
$existing = DB::table('reference_catalog_versions')->where('catalog_code', $code)->where('version_no', $version)->where('locale', 'pt-MZ')->first();
if ($existing) {
foreach ($expected as $column => $value) {
if ((string) $existing->{$column} !== (string) $value) {
throw new RuntimeException('Reference catalog version differs from its packaged source: '.$code);
}
}
return $id;
}
DB::table('reference_catalog_versions')->insert([...$expected, 'created_at' => now(), 'updated_at' => now()]);
return $id;
}
}
@@ -0,0 +1,19 @@
# ADR-001 — Identidade, autorização e acesso anónimo
- Estado: Aceite para orientar contratos e novas alterações; não declara implementação completa.
- Data: 2026-10-01
- Contexto: Laravel 13, Fortify e sessões existentes; API pública versionada e páginas Inertia. Não foi encontrado emissor/verificador JWT configurado.
## Decisão
Usar a sessão Laravel como mecanismo primário para browser/Inertia e para operações privadas same-origin, com CSRF, verificação de email quando aplicável, MFA nas áreas já protegidas e autorização explícita por Policy/Gate para cada recurso. Endpoints anónimos são uma allowlist de leitura/submissão pública, com validação, limitação de pedidos e resposta mínima. A existência de uma rota sob `/api` não a torna pública. Não introduzir JWT nem bearer tokens sem consumidor externo, ciclo de vida, audiência, rotação e revogação contratados. Não confiar em IDs enviados pelo cliente para autorizar acesso.
## Alternativas consideradas
- JWT para toda a aplicação: rejeitado; não há necessidade demonstrada e duplicaria a sessão existente.
- Um gate global de gestor: insuficiente para autorização por recurso; manter as verificações actuais como barreira adicional, com Policies por domínio.
- Tornar toda a API pública: rejeitado; publicar só operações explicitamente inventariadas.
## Consequências e validação futura
Contratos devem declarar `security` por operação e casos anónimo/autenticado/negado. Privacidade SQL/RLS é decisão separada no ADR-007. Alterações futuras precisam de testes positivos e negativos. Esta ADR não converte o gate `manage-portal` em RBAC completo.
@@ -0,0 +1,13 @@
# ADR-002 — Concorrência de edição e ETag
- Estado: Aceite como padrão para contratos de edição concorrente; adopção gradual.
- Data: 2026-10-01
- Contexto: existem campos `version_no`, revisões e verificações de versão em alguns fluxos; não há convenção `ETag`/`If-Match` global.
## Decisão
Recursos mutáveis expostos por API devem devolver um ETag forte derivado de uma representação/versionamento estável. PUT/PATCH/DELETE concorrentes exigem `If-Match`; versão ausente resulta em `428 Precondition Required`, versão desactualizada em `412 Precondition Failed`. A comparação e escrita devem ser atómicas (update condicionado à versão ou lock transaccional). Respostas devolvem novo ETag. Eventos imutáveis e criação sem edição concorrente ficam fora do requisito. Endpoints web legados podem manter `version_no` durante migração documentada; não simular ETag onde não há representação estável.
## Alternativas e consequências
Versão no body com `409` é compatível com controladores actuais mas mistura metadados e conteúdo; mantém-se apenas em contratos legados. Lock pessimista isolado não protege clientes entre pedidos. Adoptar o padrão requer testes de corrida, documentação OpenAPI e migração compatível; não altera migrations existentes nesta etapa.
@@ -0,0 +1,13 @@
# ADR-003 — Idempotência de comandos e integrações
- Estado: Aceite como padrão para operações repetíveis com efeitos.
- Data: 2026-10-01
- Contexto: há recibos idempotentes territoriais, chaves em outbox e `insertOrIgnore` localizados, mas não contrato uniforme.
## Decisão
Comandos repetíveis com efeitos aceitam `Idempotency-Key` opaca, limitada em tamanho e escopo por actor/cliente, operação e recurso. Persistir chave, hash canónico dos parâmetros relevantes, estado e resultado mínimo por janela de retenção aprovada; a mesma chave e mesmo pedido reproduzem a resposta, a mesma chave com payload diferente produz conflito. A criação do recibo e a mutação de negócio pertencem à mesma transacção. Jobs e consumidores deduplicam por identificador de evento e guardam progresso sem reexecutar efeitos já confirmados. Não registar a chave em claro em logs; guardar apenas referência/hash adequado. Retenção não é fixada aqui.
## Alternativas e consequências
Deduplicar só na memória falha em reinícios e múltiplos workers. Usar só UUID do recurso não cobre criação repetida. Integrações externas exigem reconciliação e chave do destinatário; não se promete exactly-once, mas entrega pelo menos uma vez com consumidor idempotente. Cada API define respostas de replay e conflito.
@@ -0,0 +1,12 @@
# ADR-004 — Motor seguro de expressões
- Estado: Aceite como restrição para cálculos configuráveis futuros; não existe motor geral aprovado no inventário.
- Data: 2026-10-01
## Decisão
Não executar código, SQL, PHP, JavaScript, templates ou expressões arbitrárias fornecidas por utilizador/configuração. Um futuro motor aceita apenas AST JSON versionada, com operadores e funções allowlist, tipos explícitos, unidades, limites de profundidade/custo e validação de operandos antes da avaliação. Sem reflexão, I/O, rede, acesso a relógio aleatório ou estado mutável. Avaliação determinística e pura; resultados guardam versão do AST, inputs de origem e versão do motor quando usados em decisão material. Invalidar entrada fora da gramática; sem fallback silencioso. Cada domínio aprova semântica, precisão e intervalos antes de publicar fórmulas.
## Alternativas e consequências
`eval` e SQL dinâmico são rejeitados por execução arbitrária/injecção. Um DSL de texto próprio é possível mas aumenta ambiguidade; começar por AST tipada torna contratos auditáveis. Este ADR não define fórmulas, taxas, limiares ou interpretação fiscal.
@@ -0,0 +1,13 @@
# ADR-005 — Vigência temporal e snapshots
- Estado: Aceite como convenção de contrato; aplicar por domínio.
- Data: 2026-10-01
- Contexto: schema usa `valid_from`/`valid_until`, `valid_to`, revisões e snapshots; não há convenção temporal única para todo o legado.
## Decisão
Separar tempo de validade no domínio do instante de registo/auditoria. Para novos intervalos de validade com datas, adoptar intervalo inclusivo no início e exclusivo no fim (`[valid_from, valid_to)`), `valid_from` obrigatório quando a regra de domínio o exigir e fim aberto representado por NULL; UTC/timestamptz para instantes e date para vigência civil. Snapshots imutáveis guardam versão do catálogo e IDs de origem quando resultado depende de dados versionados. Não reescrever semântica de colunas existentes (`valid_until`, `expires_at`, `published_at`) até migração contratual específica; documentar inclusividade em cada schema.
## Alternativas e consequências
Fim inclusivo cria ambiguidades em fronteiras e fusos; o intervalo semiaberto simplifica composição. Bitemporalidade integral foi adiada por não estar exigida em todas as entidades. Importações e correcções históricas preservam proveniência e não actualizam snapshots passados.
@@ -0,0 +1,13 @@
# ADR-006 — Moeda, precisão e arredondamento
- Estado: Aceite como contrato de representação; regra de cálculo requer aprovação do domínio.
- Data: 2026-10-01
- Contexto: valores exactos usam `numeric(20,2)` em partes do schema; moedas são códigos de 3 letras, mas a lista autorizada e políticas de arredondamento não estão completas.
## Decisão
Representar montante como decimal exacto em SQL e como string decimal no JSON/OpenAPI para evitar perda IEEE-754; transportar código monetário separadamente e exigir ambos ou nenhum nos campos emparelhados. `^[A-Z]{3}$` verifica formato, não validade ISO nem moeda permitida. Não converter moedas nem arredondar implicitamente. A escala, modo e momento de arredondamento dependem de regra aprovada pelo domínio e devem ser identificados em cada operação; resultados persistidos registam a moeda e, se aplicável, a regra/versão usada. Não inferir taxas fiscais, taxa de câmbio ou número de casas pela moeda.
## Alternativas e consequências
Float/double é rejeitado por erro binário. Inteiro em unidade mínima depende de escala monetária ainda não aprovada. É necessário decidir allowlist de moedas e regras específicas antes de cálculo financeiro público.
+13
View File
@@ -0,0 +1,13 @@
# ADR-007 — RLS e autorização SQL
- Estado: Aceite como gate de segurança; activação por tabela bloqueada até matriz de papéis e contexto aprovada.
- Data: 2026-10-01
- Contexto: baseline não encontrou `ENABLE/FORCE ROW LEVEL SECURITY` ou `CREATE POLICY`; permissões SQL e bypass não foram auditados. Decisões anteriores proíbem inventar roles/tenants.
## Decisão
Preservar Policies/Gates da aplicação como autorização obrigatória e não declarar RLS activo. Antes de expor dados protegidos a mais de um actor, aprovar matriz de papéis, fronteiras de isolamento/tenant, contas de runtime/migration e comportamento para jobs. Depois, aplicar RLS em tabelas e operações explicitamente mapeadas, usando papel runtime sem propriedade/`BYPASSRLS`, políticas `USING`/`WITH CHECK`, e `FORCE ROW LEVEL SECURITY` quando compatível com operações técnicas. Contexto por pedido deve ser transaccional e limpo ao devolver conexão ao pool. Garantir testes permitidos e negados, inclusive acesso SQL directo e papel privilegiado conforme ameaça.
## Alternativas e consequências
RLS genérico com tenant inventado é rejeitado; RLS desligado como se fosse controlo é igualmente rejeitado. Até a matriz e auditoria serem aprovadas, a cobertura permanece bloqueada e a aplicação não deve alegar isolamento na camada SQL. Sem alteração funcional nesta Etapa 1.
+13
View File
@@ -0,0 +1,13 @@
# ADR-008 — Outbox transaccional
- Estado: Aceite como padrão para integração assíncrona.
- Data: 2026-10-01
- Contexto: existem tabelas `outbox_events`/`inbox_events`, cliente SuiteCRM e emissão localizada; integração externa não está globalmente homologada.
## Decisão
Gravar alteração de domínio e evento outbox na mesma transacção local, com envelope versionado, identificador estável, tipo, aggregate/version, instante, classificação e payload mínimo allowlisted. Nunca incluir segredos, tokens ou PII desnecessária. Worker publica após commit, usa backoff configurado por operação, regista tentativas/erro sanitizado e marca sucesso; assumir entrega pelo menos uma vez. Consumidor valida schema/versão, deduplica por message/event ID via inbox e confirma efeito e recibo atomicamente. Falhas permanentes vão para estado de falha observável e exigem replay idempotente. Não prometer exactly-once entre sistemas.
## Alternativas e consequências
Chamada externa dentro da transacção é rejeitada por atomicidade impossível e latência. Dispatch directo após commit perde eventos em crash. Contratos e políticas de retenção/PII precisam validação por integração; existência das tabelas não significa operação activa.
@@ -0,0 +1,13 @@
# ADR-009 — Gestão segura de documentos
- Estado: Aceite como padrão de contrato e segurança; capacidades incompletas são explicitadas.
- Data: 2026-10-01
- Contexto: `documents` guarda metadados, chave de storage, SHA-256, classificação e estado; uploads privados existem em alguns fluxos, mas scanner/retention e acesso uniforme não estão demonstrados.
## Decisão
Separar metadados, blob privado e ligações tipadas. A API expõe ID opaco e metadados permitidos, nunca `storage_key` nem URL pública permanente. Autorização é avaliada em cada download pela ligação e Policy; URLs assinadas, se adoptadas, são temporárias e emitidas só depois da autorização. Upload segue quarentena, validação de tamanho/tipo real, nome seguro, hash e varrimento antimalware antes de estado descarregável. Versões são imutáveis; substituição cria nova versão, mantendo trilho. Eliminação lógica/retention segue política aprovada e não apaga automaticamente blobs ainda referenciados. Logs excluem conteúdo, nome original quando identificável, tokens e URLs assinadas.
## Alternativas e consequências
Pasta pública e path fornecido pelo utilizador são rejeitados. Preservar metadados sem scanner mantém documento não publicável até aprovação operacional. Limites, tipos permitidos, retenção e região de armazenamento devem ser definidos por domínio e legislação aplicável, sem os inventar aqui.
+261
View File
@@ -0,0 +1,261 @@
# Baseline técnico — 2026-10-01
## Âmbito e método
Auditoria somente de leitura do estado do repositório em `main`, commit `a6afb4e97ae8afc01f3455e9e67656a130c60432`. Não se inspeccionou o conteúdo de `.env` nem se ligou à base de desenvolvimento/produção. As consultas PostgreSQL abaixo usaram exclusivamente `portal_services_test` em `127.0.0.1:55441`, com o utilizador de teste `education_test` e senha vazia.
O repositório já tinha alterações funcionais locais no início da auditoria: 26 caminhos modificados e 12 não rastreados. Foram preservados. Esta entrega só cria este documento.
## Stack e versões confirmadas
| Componente | Baseline observado | Evidência |
| --- | --- | --- |
| Backend | Laravel 13.30.1 (`composer.lock`); `composer.json` exige PHP `^8.3` e Laravel `^13.17` | `composer.lock`, `composer.json` |
| PHP CLI | PHP 8.5.11; `php.ini` em `C:\Users\macbook\.config\investmoz\php-8.5.11\php.ini`; módulos `pdo_pgsql`, `pgsql`, `mbstring`, `openssl`, `intl`, `fileinfo`, `sodium`, `curl` e `zip` carregados | `php.exe -v`, `--ini`, `-m` |
| Composer | Composer 2.8.12; `composer.json` validado com o PHP 8.5.11 explícito | `composer.phar validate --no-check-publish` |
| Frontend | React 19.2.8, React DOM 19.2.8, Inertia React 3.7.0, TypeScript 5.9.3, Vite 8.2.2, Vite Plus 0.3.0, Leaflet 1.9.4 | `package-lock.json` |
| Node/npm | Node 24.20.0, npm 11.19.0 | `node --version`, `npm --version` |
| Dados | PostgreSQL 17.11 na base isolada; 147 tabelas através dos schemas `public`, `investmoz` e `skills_edu` | `artisan db:show --database=pgsql` |
| GIS | Migração requer PostGIS e CI usa `postgis/postgis:17-3.5`; a versão exacta da extensão na base isolada não foi consultada | migration `2026_09_24_000100_investmoz_extensions_functions.php`, `.github/workflows/tests.yml` |
| UI/SSR | Laravel + Inertia + React/Vite; não é Next.js | `composer.json`, `package.json`, `vite.config.ts`, `resources/js` |
O comando global `composer` resolve para o wrapper Herd/PHP 8.5.0. Para esta baseline, a validação Composer foi executada chamando `composer.phar` com o binário PHP 8.5.11 indicado pelo utilizador.
## Git e estado de trabalho
- Branch actual: `main`, a rastrear `origin/main`; as referências locais indicavam divergência `0/0` na leitura.
- Remoto configurado: `origin`.
- Branches locais adicionais observadas: `feature/existing-business-phase-0`, `feature/gis-phase-0`, `feature/mne-canonical-governance`, `feature/mne-verification-evidence` e `feature/territorial-mvp`.
- Não foi encontrada política de branching/PR versionada nesta leitura. `pull.rebase=false`; `pull.ff` não está configurado. O histórico recente contém commits directos em `main`; isto descreve evidência Git, não recomenda o processo.
- Estado inicial: 26 caminhos modificados e 12 não rastreados. Entre eles há controllers, services, rotas, migração, React, testes e documentação da implementação de negócios existentes. A auditoria não os alterou nem os atribui ao trabalho deste relatório.
## Migrations e base de dados
`artisan migrate:status --database=pgsql` foi executado com estas substituições de ambiente, sem ler as credenciais de `.env`:
```powershell
$env:DB_CONNECTION='pgsql'
$env:DB_DATABASE='portal_services_test'
$env:DB_HOST='127.0.0.1'
$env:DB_PORT='55441'
$env:DB_USERNAME='education_test'
$env:DB_PASSWORD=''
$env:DB_URL=''
& 'C:\Users\macbook\.config\investmoz\php-8.5.11\php.exe' artisan migrate:status --database=pgsql
```
Resultado: 52 migrations listadas como `Ran`; zero pendentes **nesta base de teste**. `artisan db:show` confirmou PostgreSQL 17.11, `portal_services_test`, porta 55441 e 147 tabelas. Isto não comprova o estado de migrations em desenvolvimento, staging ou produção. O repositório contém 52 ficheiros de migration no momento da auditoria. Migrações posteriores à linha base podem ainda estar apenas no working tree; não foram aplicadas a outra base.
## Estrutura e convenções observadas
- Backend organizado em `app/Http/Controllers`, `Requests`, `Resources`, `Policies`, `Services`, `Jobs`, `Console/Commands`, `Integrations` e `Models`.
- React/Inertia em `resources/js/pages`, componentes em `resources/js/components`; rotas separadas por domínio em `routes/`.
- PostgreSQL/PostGIS é definido principalmente por migrations Laravel com SQL PostgreSQL explícito. A migration de extensões instala `postgis` e `pgcrypto`.
- Existe uma migração canónica de negócios existentes, além de catálogos de oportunidades, CRM, geografia e território. Não foi presumida uma segunda stack.
- Catálogos e território têm documentação de dados/OpenAPI por domínio; não foi localizado um contrato OpenAPI canónico na raiz que cubra todas as rotas.
- Os artefactos OpenAPI existentes são parciais e misturam versões 3.0.3/3.1.0. Há geradores locais, mas o workflow CI não apresenta validação semântica global dos contratos.
## Autenticação, autorização e segurança de dados
- Laravel Fortify fornece login, verificação de email, recuperação de password, 2FA e passkeys configuráveis.
- `manage-portal` é um Gate baseado em email verificado e lista configurada. Rotas de gestão usam também middleware `can:manage-portal`; alguns domínios sensíveis acrescentam MFA e designações específicas.
- A autorização é principalmente aplicada na aplicação via middleware, Gate/Policy e serviços. A pesquisa de migrations não encontrou `ENABLE ROW LEVEL SECURITY`, `FORCE ROW LEVEL SECURITY` ou `CREATE POLICY`.
- Portanto, isolamento RLS não está demonstrado; a aplicação não deve ser considerada protegida por RLS. Privilégios SQL por papel e teste de bypass de RLS não foram auditados porque não se acedeu a ambientes fora do banco isolado.
- Existem funções e triggers PostgreSQL para integridade/auditoria. Estes mecanismos não substituem políticas RLS.
## OpenAPI, integrações e catálogos
- OpenAPI está documentado por áreas, incluindo negócio existente, registo territorial, zonas e unificação do investimento. Os geradores estão em `tools/` e os JSON em `docs/investmoz/`.
- Cobertura, versionamento e validação são específicos de cada domínio; não há evidência de contrato único para todas as rotas web/API ou verificação OpenAPI global no CI.
- SuiteCRM tem cliente HTTP, configuração OAuth e comando de probe. `SUITECRM_ENABLED` é falso por omissão em `.env.example`; esta leitura não confirmou credenciais nem conectividade CRM.
- O schema tem `outbox_events`, `inbox_events`, `sync_runs` e ligações externas. A existência dessas tabelas/clientes não demonstra um consumidor completo, integração homologada ou sincronização operacional de todos os domínios.
- Há catálogos de referência, sectores, oportunidades e território. A existência de dados/seeders não comprova por si só processo completo de versionamento e aprovação para cada catálogo.
## CI e testes
Workflow observado: `.github/workflows/tests.yml`, executado em push para `main` e pull request. Usa PostgreSQL/PostGIS 17/3.5, PHP 8.5, Node 24; instala Composer/npm, compila assets, chama `composer ci:check`, executa ciclo de schema PostgreSQL/PostGIS opt-in e testes API frontend. A configuração indica `coverage: none`; não há evidência de limiar de cobertura.
Inventário local: 36 ficheiros de testes Feature e 5 Unit, mais testes Node. `phpunit.xml` inclui Feature e Unit; `phpunit.investmoz.xml` isola `tests/InvestMoz` para ciclo de schema destrutivo protegido. Os testes Laravel fazem reset apenas em banco de teste reconhecido pela guarda de `tests/TestCase.php`.
**Gate executado nesta auditoria:**
| Comando | Resultado |
| --- | --- |
| PHP 8.5.11 `-v`, `--ini`, `-m` | Aprovado; caminho/configuração e extensões verificados |
| Composer 2.8.12 `validate --no-check-publish`, invocado pelo PHP 8.5.11 | Aprovado: `composer.json is valid` |
| `artisan migrate:status --database=pgsql` no banco de teste 55441 | Aprovado: 52 Ran, zero pendentes nessa base |
| `artisan db:show --database=pgsql` no banco de teste 55441 | Aprovado: PostgreSQL 17.11; 147 tabelas |
| `npm.cmd run check` | **Falhou:** Vite Plus reportou problemas de formatação em 108 ficheiros. Não foi aplicado autofix. A execução parou aqui, conforme o gate pedido. |
| PHPStan, Pint global, suite PHPUnit completa, cobertura, build e execução GitHub Actions | Não executados nesta auditoria após o gate falhar; sem resultados novos a declarar. |
As pastas/ficheiros listados pelo Vite Plus incluem código e documentos de diversas áreas, inclusive páginas e artefactos já alterados no working tree. Não foram modificados para preservar o escopo da Etapa 0.
## Matriz de bloqueadores
| Severidade | Bloqueador e evidência | Responsável por função | Condição de desbloqueio |
| --- | --- | --- | --- |
| Crítica — gate actual | `npm.cmd run check` falha em 108 ficheiros; o CI `composer ci:check` inclui esta verificação. | Responsável frontend / mantenedor CI | Rever os ficheiros reportados, acordar formatação sem autofix indiscriminado, obter `npm run check` verde e executar novamente o workflow. |
| Alta | Working tree em `main` com 26 modificados e 12 não rastreados; inclui código, migration e testes não commitados. | Responsável de release / mantenedor Git | Identificar autoria e intenção, rever diff, separar trabalho por branch/PR e manter artefactos aprovados. |
| Alta | Estado de migrations só foi confirmado em `portal_services_test`; nenhum acesso/estado de dev, staging ou produção foi verificado. | DBA / responsável de release | Consultar cada ambiente com acesso read-only aprovado e arquivar `migrate:status`/inventário, sem inferir pelo banco QA. |
| Alta | Nenhuma política RLS foi encontrada nas migrations; isolamento depende das barreiras da aplicação e os privilégios SQL não foram auditados. | Arquitecto de segurança / DBA | Definir modelo de papéis e ameaça, decidir RLS ou controlo compensatório documentado, testar acesso directo e bypass com papéis reais. |
| Alta | Suite completa, PHPStan/Pint e cobertura não foram executados após o gate falhar; CI declara `coverage: none`. | Mantenedor CI / QA | Depois de resolver e aprovar o gate de formatação, correr CI completo em ambiente isolado; decidir e documentar política de cobertura. |
| Média | OpenAPI fragmentado por domínio, versões 3.0.3/3.1.0 e sem validação global observada no workflow. | API lead / arquitectura | Definir contratos canónicos/versionamento, cobertura de rotas, validação e testes de compatibilidade no CI. |
| Média | SuiteCRM está desactivado por omissão e a auditoria não confirma homologação/consumidor para todos os eventos. | Responsável de integração / CRM | Confirmar ambiente e credenciais por processo seguro, mapear eventos/dados, idempotência/reconciliação e executar homologação sem dados reais. |
| Média | Política de branches/PR não está documentada; commits recentes estão directamente em `main`. | Mantenedor do repositório | Aprovar política de revisão, protecção de branch, releases e rollback e registá-la no repositório. |
Responsáveis são funções, não pessoas; não foram inferidos nomes nem delegações.
## Reprodutibilidade, limites e rollback
Comandos relevantes usados: `git status --short --branch`, `git branch -vv`, `git log -5 --oneline --decorate`, `git rev-list --left-right --count main...origin/main`, leitura de `composer.json`/locks, `package.json`/lock, `phpunit*.xml`, migrations, policies, rotas e workflow; `php.exe -v`, `--ini`, `-m`; Composer validate pelo PHP 8.5.11; migrations status e `db:show` com overrides para `portal_services_test`; `npm.cmd run check`.
As consultas SQL foram read-only. `npm run check` foi verificação sem `--fix`. O único artefacto criado é `docs/implementation/baseline.md`; rollback: remover esse ficheiro e a pasta `docs/implementation` se ficar vazia. Não há migration, alteração funcional, teste ou dado de produção a reverter.
## Estado proposto do gate
**Etapa 0: baseline documentado; gate técnico BLOQUEADO para avançar.** A evidência suficiente para descrever stack e estado local foi recolhida, mas a verificação frontend falhou e existem bloqueadores críticos/altos por resolver. Não se propõe iniciar a Etapa 1 até revisão humana e desbloqueio da matriz.
## Gate da Etapa 1 — 2026-10-01
**Resultado: BLOQUEADO; Etapa 1 não iniciada.** A pré-condição expressa pelo pedido — Etapa 0 aprovada e bloqueadores arquitecturais resolvidos — não está satisfeita: o estado registado acima continua bloqueado e não há evidência de aprovação humana ou de resolução dos bloqueadores. Em particular, o comando `npm.cmd run check` falhou na baseline e a decisão sobre RLS/privilégios continua pendente. O working tree também foi registado como sujo em `main`; o estado de migrations em produção não foi verificado.
Por respeito ao gate, não foram criados ADRs nem o mapa SQL–OpenAPI, e não foi iniciada implementação funcional. Não foram executados novos testes nesta etapa, pois fazê-lo não resolveria nem substituiria a aprovação e os desbloqueios exigidos. Para retomar: rever e fechar a matriz de bloqueadores, obter aprovação explícita da Etapa 0 e só então executar a inspecção de contratos da Etapa 1.
## Execução da Etapa 1 — 2026-10-01
O utilizador instruiu assumir a Etapa 0 aprovada e autorizou as etapas subsequentes. Com base nessa autorização, a Etapa 1 foi executada apenas no âmbito documental; este registo actualiza o gate anterior, que permanece como histórico da auditoria inicial.
ADRs registados como aceites para orientar contratos e trabalho futuro:
- [ADR-001 — IAM e acesso anónimo](adr/ADR-001-iam-anonymous-access.md)
- [ADR-002 — ETag e optimistic locking](adr/ADR-002-etag-optimistic-locking.md)
- [ADR-003 — Idempotência](adr/ADR-003-idempotency.md)
- [ADR-004 — Motor seguro de expressões](adr/ADR-004-safe-expression-engine.md)
- [ADR-005 — Vigência temporal e snapshots](adr/ADR-005-temporal-validity.md)
- [ADR-006 — Moeda e arredondamento](adr/ADR-006-currency-rounding.md)
- [ADR-007 — RLS](adr/ADR-007-rls.md)
- [ADR-008 — Outbox](adr/ADR-008-outbox.md)
- [ADR-009 — Gestão de documentos](adr/ADR-009-document-management.md)
O [mapa SQL–OpenAPI](sql-openapi-map.csv) parte das 684 linhas do dicionário `docs/investmoz/data-dictionary.csv` e contém uma linha por campo, tipo/nulabilidade/restrições SQL, descrição, migration de origem, ponteiro/tipo/formato OpenAPI quando há coincidência nominal e estado de classificação dos dados. O cruzamento nominal encontrou 294 candidatos que ainda precisam validação semântica e 390 campos sem propriedade OpenAPI documentada. A classificação por campo não existe no dicionário de origem e está marcada como não registada; não foi inferida. Portanto, o inventário é completo, mas o requisito de correspondência SQL–OpenAPI sem lacunas **não foi satisfeito**. A matriz também deixa explícito que não se deve expor automaticamente campos SQL privados/internos.
Não foram alterados contratos OpenAPI existentes, migrations, aplicação, configurações ou dados. Não foi executado teste funcional. Revisão documental efectuada por contagem do CSV (684 linhas de dados), cruzamento mecânico exacto de nomes contra propriedades nos cinco OpenAPI JSON e leitura dos nove ADRs. O próximo gate da Etapa 1 requer confirmar candidatos, declarar por campo se é exposto ou interno, completar contratos autorizados e obter classificação de dados aprovada por domínio, sem inventar campos nem classificações.
**Estado proposto:** Etapa 0 assumida aprovada por instrução do utilizador; ADRs da Etapa 1 registados como aceites; gate de conclusão documental da Etapa 1 **PENDENTE** até fechar os 390 campos sem contrato, validar os 294 candidatos e aprovar classificações. Não iniciar estrutura persistente ou interface pública baseada em campos ainda não aprovados.
## Gate da Etapa 2 — 2026-10-01
**Resultado: BLOQUEADO antes de alterações de schema.** A aprovação das etapas anteriores foi assumida conforme instrução explícita do utilizador. Contudo, a pré-condição técnica desta etapa exige PostgreSQL 16 com PostGIS disponível para fresh/rollback. A base isolada acessível (`portal_services_test`, `127.0.0.1:55441`) reportou PostgreSQL 17.11; o único serviço local observado foi `postgresql-x64-17` (Running), não foi encontrado serviço PostgreSQL 16 e o comando `docker` não está disponível. A versão exacta de PostGIS nesta base não foi confirmada: a tentativa de consulta via PsySH falhou por erro de sintaxe/citação. A migration existente `2026_09_24_000100_investmoz_extensions_functions.php` já instala `postgis`/`pgcrypto` e define três funções; não foi alterada nem repetida.
Não foram criadas migrations ou tabelas e não foram executados testes de fresh/rollback, pois não existe o servidor PostgreSQL 16 requerido para validar compatibilidade real. Para desbloquear, disponibilizar um PostgreSQL 16 isolado com PostGIS, acesso de teste e possibilidade de criar/eliminar schema/base; confirmar a versão da extensão antes de implementar. Nenhum serviço de domínio deve ser migrado nesta etapa.
**Estado proposto:** Etapa 2 **BLOQUEADA por ambiente incompatível/não confirmado**. Rollback desta tentativa: remover somente esta secção da baseline; não há alterações de schema ou dados a reverter.
## Execução da Etapa 2 — PostgreSQL 17.11 — 2026-10-01
O utilizador corrigiu o requisito: a versão-alvo é PostgreSQL 17.11, não 16. A aprovação das etapas precedentes foi assumida pela instrução explícita do utilizador. O ambiente foi confirmado como PostgreSQL 17.11, PostGIS 3.6.2 e pgcrypto 1.3; existe o tipo PostGIS `geometry`/`geography` e estão instaladas as três funções `investmoz_*` existentes.
### Decisão de escopo
Não foi criada migration nova. A migration `database/migrations/2026_09_24_000100_investmoz_extensions_functions.php` já instala as duas extensões com `IF NOT EXISTS` e cria as funções utilitárias necessárias. O schema e testes existentes usam/verificam UUID, JSONB e tipos espaciais. Não existe decisão de domínio que justifique novo ENUM ou DOMAIN; criar tipos não usados acrescentaria objectos persistentes sem benefício demonstrado. O `down()` existente remove as funções próprias com `RESTRICT` e conserva extensões partilhadas, como confirmou o teste de ciclo. Migrations existentes não foram reescritas.
### Ficheiros e verificações
- `tests/InvestMoz/SchemaLifecycleTest.php`: removido `--seed` global das duas operações limitadas ao baseline histórico; esse seeder depende de `investment_reference_options`, criada depois do baseline. O teste continua a invocar explicitamente `InvestMozReferenceSeeder` no ponto em que as tabelas correspondentes existem.
- Comando: `C:\Users\macbook\.config\investmoz\php-8.5.11\php.exe vendor/bin/phpunit --configuration=phpunit.investmoz.xml --do-not-cache-result`, com `INVESTMOZ_SCHEMA_TESTS=1` e ligação à base temporária `investmoz_test` no porto 55441.
- Primeira execução falhou antes das asserções porque `DatabaseSeeder` tentou inserir em tabela posterior ao baseline. Após corrigir o harness, resultado: **1 teste aprovado, 3.106 asserções, 0 falhas**. O caso executa fresh, rollback por etapas, preservação de extensões partilhadas e reaplicação das migrations.
- Consulta read-only na base de ciclo: `server=17.11`, `postgis=3.6.2`, `pgcrypto=1.3`, tipos `geometry/geography=2`, funções utilitárias próprias `=3`.
- A base temporária `investmoz_test`, inicialmente ausente, foi criada apenas para o teste e removida no fim; a confirmação posterior encontrou zero bases com esse nome. A base pré-existente `portal_services_test` não foi limpa ou modificada por este ciclo.
- Nenhuma tabela de domínio nova foi criada por esta etapa; o teste apenas recriou schema na base descartável e esta foi eliminada após a verificação.
**Estado proposto:** Etapa 2 **CONCLUÍDA para a fundação actualmente necessária em PostgreSQL 17.11/PostGIS 3.6.2**. Extensões, tipos espaciais, funções, fresh/rollback/reaplicação foram verificados. Não foram introduzidos enums/domínios novos por falta de necessidade contratual aprovada. Rollback desta entrega: reverter a alteração do teste; não há migration nova. A anotação documental pode ser removida sem afectar schema ou aplicação.
## Execução da Etapa 3 — Catálogos e geografia — 2026-10-01
### Implementação
Foram adicionadas migrations para `reference_catalog_versions`, `authorities` e `economic_activities`, com chaves estrangeiras para organizações, territórios e sectores existentes, índices, `pt-MZ`, vigência `[valid_from, valid_to)` e protecção contra alteração de versões publicadas. Não foram duplicados os `geo_locations` nem os catálogos de sectores já existentes.
O seeder `ReferenceCatalogSeeder` é determinístico e idempotente: valida a origem/versionamento geográfico já empacotado, cria a versão de actividades económicas com referências aos seis sectores disponíveis e deixa autoridades sem registos fictícios enquanto não houver fonte oficial aprovada. As actividades são agrupamentos do portal, não uma classificação estatística/legal oficial.
Foram expostos endpoints anónimos de leitura para autoridades, jurisdições e actividades económicas, com pesquisa, paginação, versão, metadados `pt-MZ` e ETag. Jurisdições respeitam o filtro pai-filho e só expõem referências públicas. A UI territorial passou a consumir o endpoint versionado e limpa a selecção e descendentes quando o pai muda. O contrato está em `docs/investmoz/catalogues/openapi.json`.
### Ficheiros desta etapa
- Criados: migration `2026_10_01_000100_create_reference_catalogs.php`; seeder `ReferenceCatalogSeeder.php`; controller `Api/V1/ReferenceCatalogController.php`; contrato OpenAPI `docs/investmoz/catalogues/openapi.json`; helper JS e declaração de tipos `resources/js/lib/territory-path.js` / `.d.ts`; testes PHP `ReferenceCatalogTest.php` e Node `territory-path.test.mjs`.
- Alterados: `ProductionReferenceSeeder.php`, `routes/api.php`, `resources/js/components/investment-territory.tsx`, `resources/js/pages/investment/catalogue.tsx`.
### Comandos e evidências
- PHP 8.5.11 explícito: suíte PHPUnit focada (`ReferenceCatalogTest|InvestmentTerritoryTest|TerritorialRegistryTest`) — **22 testes, 188 asserções, aprovado**.
- `node --test tests/frontend/territory-path.test.mjs tests/frontend/portal-api.test.mjs` — **15 testes, aprovado**; os testes do helper de geografia isolados — **2 aprovados**.
- `npm.cmd run types:check` — aprovado.
- `npm.cmd run build` — aprovado; aviso não bloqueante de ausência do pacote opcional `fontaine`.
- Laravel Pint em modo `--test` nos ficheiros PHP novos/alterados — aprovado.
- Contrato OpenAPI JSON e referências locais — parse/validação estrutural executados com PowerShell; válido.
- `git diff --check` ainda por executar: o executável Git não está no PATH desta sessão; foi localizado em `C:\Program Files\Git\cmd\git.exe` para verificação final.
- Os testes Laravel usaram a base descartável `investmoz_test`, inicialmente ausente, no serviço PostgreSQL 17.11/PostGIS 3.6.2; a base será removida após a execução. A base preexistente `portal_services_test` não foi resetada nesta etapa.
### Riscos, pendências e rollback
Não há nomes de autoridades sem fonte oficial aprovada; o endpoint informa catálogo ainda não publicado até haver dados e aprovação. O catálogo de actividades não substitui códigos oficiais nacionais/internacionais. O filtro territorial depende da classificação pública existente no pacote cartográfico; confirmar versão/origem com o responsável de dados antes da publicação operacional. Não foi aplicado migration em base de produção.
Rollback: executar `php artisan migrate:rollback --step=1` apenas num ambiente onde esta migration seja a última aplicada e após exportar/publicar nenhum conteúdo de catálogo; o `down()` protege-se contra remoção de dados de autoridade/actividade. Reverter também o seeder, controller/rotas, contrato e consumo UI desta etapa. O deploy deve publicar primeiro migration/seeds e só depois a UI que chama os endpoints.
**Estado proposto:** Etapa 3 tecnicamente implementada e testes aprovados; gate de publicação **PENDENTE** da revisão humana sobre fonte e publicação dos dados de autoridades e da aprovação da classificação de actividades. Não iniciar a etapa seguinte sem fechar esse gate.
## Gate da Etapa 6 — Simulações, jobs, relatórios e segurança — 2026-10-01
**Resultado: BLOQUEADO antes de alterações funcionais.** A inspecção confirmou Laravel 13.30.1, PHP CLI 8.5.11, React 19 com Inertia, PostgreSQL 17.11/PostGIS 3.6.2 e CI com PostgreSQL/PostGIS 17. O working tree já contém alterações não commitadas anteriores; foram preservadas.
### Bloqueadores de pré-condição
1. **Modelo de parâmetros não aprovado.** Não foi localizado um contrato aprovado que defina questionários/versionamento, tipos de resposta, semântica de cenários, snapshots de entradas e resultados, nem os limites e estados de simulação. O ADR-004 decide que expressões devem ser seguras, mas não aprova o modelo funcional de parâmetros. Sem isso, criar `questionnaire_versions`, `simulations`, `scenarios`, `results`, `result_sources` ou `obligations` exigiria inferir estrutura e significado.
2. **Matriz RLS/IAM ausente.** O ADR-007 declara explicitamente RLS bloqueado até aprovação de papéis, fronteiras de isolamento/tenant, papéis de runtime/migration e contexto de jobs. `docs/investmoz/decisions.md` também proíbe inventar roles, tenants ou policies. A baseline não encontrou políticas RLS activas. Não há autorização para assumir qual actor pode ver cada execução ou dado de relatório.
3. **Contratos operacionais por domínio pendentes.** Os ADRs-002 e -003 aprovam padrões de ETag/If-Match e `Idempotency-Key`, mas deixam detalhes como representação/versionamento do recurso, escopo e retenção do ledger e replay por operação para cada API. Os requisitos de jobs também não fixam filas, limites de retry/backoff, idempotência de efeitos nem regras de reconciliação. Não foram encontrados contratos aprovados desta Etapa 6 que fechem esses detalhes.
4. **Soft delete e token anónimo.** Não foi identificada decisão que determine em quais tabelas se aplica soft delete nem como o hash/token anónimo se relaciona com autenticação, escopo, retenção e revogação. O ADR-001 não permite tratar IDs enviados pelo cliente como autorização.
A autorização anterior para considerar aprovadas as etapas sequenciais não fornece os valores e decisões de domínio ausentes, e não revoga as condições explícitas dos ADRs. Por isso, nenhum schema ou comportamento foi inferido. Não foram executados migrations, seeds ou testes funcionais desta etapa.
### Plano proposto após desbloqueio
Após aprovação do modelo de parâmetros e das matrizes IAM/RLS, inspeccionar novamente os contratos aprovados e apresentar antes de editar a lista exacta de migrations, modelos, serviços, policies, jobs, rotas/OpenAPI e testes. A validação deverá incluir acesso autorizado e negado por actor/organização, teste SQL directo com papel runtime e contexto limpo entre transacções, ETag ausente/desactualizado, replay e conflito de idempotência, concorrência real, retry limitado, reconciliação após falha e auditoria sem PII. Até lá, não iniciar estes artefactos.
**Condição para desbloquear:** aprovar o esquema de parâmetros e snapshots; fornecer matriz actor × recurso × operação e tenant/contexto; aprovar papéis PostgreSQL separados, grants, comportamento de jobs e estratégia de conexão/pool; definir escopo/retensão de idempotência, política de soft delete e ciclo de vida do token anónimo; fixar retries/reconciliação por job.
**Estado proposto:** Etapa 6 **BLOQUEADA por pré-condições arquitecturais em falta**. Alteração desta tentativa: somente esta secção documental. Rollback: remover esta secção de `baseline.md`; não há alteração de código, migration, dado ou ambiente a reverter.
## Execução da Etapa 6 — Simulações, jobs, relatórios e segurança — 2026-10-01
O utilizador instruiu assumir todas as aprovações anteriores. A secção de gate bloqueado acima fica como registo histórico e é substituída para esta execução pela aprovação assumida explicitamente nesta conversa. A inspecção confirmou Laravel 13.30.1, PHP 8.5.11, React 19/Inertia, PostgreSQL 17.11/PostGIS 3.6.2 e os padrões locais de membership, outbox, idempotência, auditoria e filas. Não foram alteradas migrations anteriores nem `.env`.
### Diff lógico
- Migration aditiva `2026_10_01_000200_create_simulation_execution_core.php`: cria versões de questionário, simulações, cenários, resultados/fontes, obrigações, snapshots de relatório, ledger de jobs, recibos idempotentes e auditoria append-only. Inclui FKs, unicidades, hashes, RLS habilitado e `FORCE ROW LEVEL SECURITY`, policies separadas de leitura/escrita e triggers de imutabilidade para questionário publicado, resultados, fontes, relatório pronto e auditoria. `down()` recusa rollback se houver dados, excepto a versão base fixa que a própria migration instala.
- Questionário publicado `user-entered-arithmetic` v1 (`pt-MZ`): os três parâmetros aritméticos são entradas do utilizador. A taxa e redução ficam limitadas ao intervalo já usado pelo simulador existente (0–100); a base não tem tecto fiscal configurado. A fórmula é `base × taxa ÷ 100 × (1 − redução ÷ 100)`. A resposta não infere moeda, taxa legal, elegibilidade ou obrigação jurídica.
- Models Eloquent novos: `QuestionnaireVersion`, `Simulation`, `SimulationScenario`, `SimulationResult`, `SimulationResultSource`, `SimulationObligation`, `SimulationReport` e `SimulationJob`.
- `SimulationExecution` faz transacções, lock/advisory lock por chave, replay idempotente e conflito para payload divergente; o token anónimo é HMAC-SHA256 e só o hash chega à base/fila. Contexto RLS é `SET LOCAL` e é limpo com o fim da transacção. Edição compara ETag em lock transaccional, devolve 428/412 e incrementa versão; conclusão/falha do job também avança versão e ETag.
- `RunSimulation` tem três tentativas, backoff `[10,60]`, estado reconciliável e erros persistidos apenas como códigos. Resultados, proveniência de entradas e relatórios guardam snapshots/hash imutáveis. Não grava texto de excepção, token ou PII no log de auditoria.
- `SimulationPolicy` e policies SQL reaproveitam `memberships` e os papéis existentes (`admin`, `manager`, `officer`, `viewer`); não criam tenants ou papéis novos. Membros podem ler simulações da organização; escrita exige owner pessoal, token válido ou membership com papel de escrita. Revogar membership retira acesso mesmo ao criador de uma simulação organizacional.
- `SimulationController`/requests/rotas API adicionam consulta da versão publicada, criação/consulta, edição de cenário e consulta do relatório. `routes/api.php` activa middleware web/sessão, CSRF e throttle nos endpoints do simulador.
- `docs/investmoz/simulations/openapi.json` documenta cabeçalhos, payloads e resultados HTTP. `SimulationExecutionTest.php` cobre os fluxos e o ciclo vazio de rollback/reaplicação.
Ficheiros criados: a migration acima; oito models; `app/Policies/SimulationPolicy.php`; `app/Http/Requests/StoreSimulationRequest.php`; `app/Http/Requests/UpdateSimulationScenarioRequest.php`; `app/Http/Controllers/Api/V1/SimulationController.php`; `app/Services/SimulationExecution.php`; `app/Jobs/RunSimulation.php`; `docs/investmoz/simulations/openapi.json`; `tests/Feature/SimulationExecutionTest.php`. Ficheiros alterados: `routes/api.php` e este baseline.
### Comandos e evidências
- PHP 8.5.11 explícito, PHPUnit em `investmoz_test` isolada no PostgreSQL 17.11/PostGIS 3.6.2: `vendor/bin/phpunit --configuration=phpunit.xml --filter='SimulationExecutionTest|ReferenceCatalogTest|InvestmentTerritoryTest|TerritorialRegistryTest' --do-not-cache-result` — **32 testes, 252 asserções aprovados**. Inclui down/up da migration sem dados.
- PHPStan nos ficheiros da etapa — **0 erros**.
- Laravel Pint `--test` nos ficheiros PHP da etapa — aprovado.
- OpenAPI JSON e referências locais — parse e verificação estrutural aprovados.
- `artisan route:list --path=api/v1/simulations` — quatro rotas de simulação listadas; rota de questionário confirmada pelo teste de endpoint.
- Teste de RLS foi executado sob role PostgreSQL temporário `NOLOGIN`, sem `BYPASSRLS`, com grants mínimos a `simulations`/`memberships`: owner autorizado lê, actor não relacionado não lê, viewer da organização lê mas não actualiza, e token anónimo incorrecto não lê. A role temporária foi removida pelos testes.
- Teste de optimistic locking verifica ausência de `If-Match` (428), ETag antigo (412), edição concorrente com ETag original após outra edição (412) e incremento/reexecução com ETag actual.
- Testes idempotentes cobrem replay igual e conflito de payload; retries confirmam limite três/backoff e reconciliação terminal. Hashes de resultados/fontes são comparados contra a representação canónica PostgreSQL; teste tenta actualizar auditoria e confirma que o trigger append-only rejeita a alteração.
- `investmoz_test` estava ausente antes desta execução e foi criada apenas para estes testes. A base preexistente `portal_services_test` não foi limpa nesta etapa. Não se ligou a desenvolvimento, staging ou produção.
### Riscos, limites e rollback
Os registos de `simulation_obligations` não são auto-gerados sem fonte versionada aprovada; a tabela não transforma orientação em obrigação jurídica. O relatório nesta entrega é snapshot JSON com hash, não PDF/documento assinado. O motor limita-se à fórmula aritmética já apresentada no portal e só usa valores introduzidos pelo utilizador. Não houve verificação dos GRANT/roles reais de produção; a conta de runtime deve ser não-proprietária e sem `BYPASSRLS`, enquanto a conta de migration administra schema. O worker recebe somente ID de simulação, actor e hash de token, nunca o token em claro.
Rollback: primeiro retirar o consumo das rotas, depois executar rollback direccionado à migration `2026_10_01_000200_create_simulation_execution_core.php` com o comando Artisan de rollback por `--path` no ambiente pretendido. O `down()` só prossegue quando as tabelas não contêm dados de negócio; preservar/exportar simulações e relatórios antes de rollback com dados. O ciclo vazio down/up foi testado; rollback com dados deve falhar de forma segura e não foi executado sobre dados de negócio.
**Estado proposto:** Etapa 6 **IMPLEMENTADA; gate técnico VERDE** para revisão humana. Não foi iniciado trabalho de etapa posterior nem feito deploy.
@@ -0,0 +1,104 @@
# Simulador Fiscal InvestMoz — Governação do MVP
**Fase:** 0 — Governação
**Estado:** gate da Fase 0 **APROVADO PARA CONTINUAÇÃO** por instrução explícita do utilizador em 2026-10-01; blockers específicos de domínio continuam sujeitos aos gates das fases correspondentes.
**Data da inspecção:** 2026-10-01
**Branch de trabalho:** `codex/simulador-fiscal-phase-0`
## Objectivo e limites
Esta governação define o MVP fiscal sem substituir funcionalidades existentes do Portal do Investidor. O âmbito funcional do Simulador Fiscal fica limitado ao que está aprovado nesta especificação; não declara homologação, publicação de regras, integração operacional ou cobertura fiscal que não tenha evidência.
| Tratamento | Âmbito aprovado para o MVP |
| --- | --- |
| Calcular | IRPC; IRPS sobre remunerações; IVA; ISPC; retenções na fonte; INSS; valor aduaneiro; direitos aduaneiros; ICE; IVA na importação; obrigações fiscais/cambiais; cenários; relatórios PDF e JSON. |
| Identificar e orientar, sem cálculo automático | Imposto do selo; impostos autárquicos; mais-valias; preços de transferência; convenções completas de dupla tributação; benefícios complexos ZEE/ZFI; regimes sectoriais. |
| Fora desta release | Manifestações formais; oportunidades de negócio; conversões para projecto/oportunidade; CRM completo; conta avançada do investidor; assistente inteligente. |
Decisão de escopo registada em 2026-10-01: a especificação limita o MVP fiscal; funcionalidades existentes fora desse limite são preservadas no portal, mas não integram o MVP nem o fluxo anónimo fiscal.
## Baseline técnico inspeccionado
- Backend Laravel 13.30.1; PHP 8.5.11 CLI; React 19/Inertia/Vite; Node 24.20.0 e npm 11.19.0. Não é uma aplicação Next.js.
- `composer.lock` e `package-lock.json` existem. `vendor/` e `node_modules/` estão presentes. O comando global `composer` não foi encontrado nesta sessão; não reinstalar dependências.
- PostgreSQL/PostGIS está configurado no workflow CI e em `deploy/docker/compose.yaml`. O serviço local não foi consultado nesta execução; `docker` não está disponível no PATH.
- Redis e armazenamento S3 aparecem como opções/configuração, mas não como serviços activos no compose de desenvolvimento inspeccionado. Não foi confirmada conectividade a IAM/SSO externo.
- O workflow CI usa PHP 8.5, Node 24 e PostgreSQL/PostGIS 17/3.5. O PHPUnit 12.5.34 está instalado.
- Existem contratos OpenAPI por domínio: alguns 3.1.0 e outros 3.0.3. Os cinco ficheiros JSON inspeccionados são parseáveis; não foi executada validação semântica integral nem verificada equivalência API/schema.
- Há migrations PostgreSQL para vários domínios, ADRs 001–009 e `sql-openapi-map.csv`. O ADR-007 mantém RLS bloqueado por falta de matriz aprovada; migrations locais do simulador incluem policies RLS. A divergência entre decisão e artefactos requer revisão de segurança antes de declarar RLS demonstrado.
- `.env.example` foi lido apenas para inventariar nomes de configuração. `.env`, segredos e serviços de produção não foram inspeccionados nem usados nesta fase.
## Matriz de conformidade e rastreabilidade
| Requisito/fase | Evidência actual | Estado | Lacuna ou divergência | Ficheiros/áreas de referência | Próximo gate |
| --- | --- | --- | --- | --- | --- |
| Fase 0 — governação | Baseline, decisões e ADRs; este documento acrescenta escopo, RACI funcional, riscos, backlog e gates | CONFORME PARA INICIAR A FASE 1 | Responsáveis nominais e aprovações fiscal/jurídica/segurança/ops continuam necessários antes dos gates que delas dependem | `docs/implementation/baseline.md`, `docs/implementation/adr/`, este ficheiro | Gate da Fase 1 |
| Fase 1 — fundação e contratos executáveis | Migrations, mapa SQL/OpenAPI, contratos por domínio e teste de ciclo de schema | PARCIAL | Contratos fragmentados em 3.0.3/3.1.0; validação integral e reconciliação de todos os caminhos/colunas não demonstradas neste gate | `database/migrations/`, `docs/implementation/sql-openapi-map.csv`, `docs/investmoz/*/openapi.json` | Inventário aprovado, validação OpenAPI integral e testes seguros em DB descartável |
| Fase 2 — repositório legislativo | Existe rota de consulta de fontes legislativas e registos jurídicos noutros domínios | PARCIAL | Não há evidência suficiente de repositório fiscal versionado com disposições, vigência, hashes, relações, ingestão e pesquisa integral | `app/Http/Controllers/Api/V1/LegislationSourceController.php`, migrations, `routes/api.php` | Contrato e fontes oficiais aprovadas; verificar CRUD/importação e integridade |
| Fase 3 — regras e publicação | Há catálogos versionados e padrões de revisão noutros domínios | AUSENTE para regras fiscais | Não foi identificada evidência de rule sets fiscais, parâmetros com fonte oficial, elegibilidade, revisão de quatro olhos, testes dourados e publicação imutável | migrations, models/services fiscais (a inventariar na Fase 3) | Aprovação de fontes, regras, responsabilidade fiscal/jurídica e vigência |
| Fase 4 — API e formulário | `/Simulador`, catálogos, snapshots e execução aritmética existem | BLOQUEADO por divergência | O formulário contém identidade/contactos e finalidade de manifestação/oportunidade; a especificação limita o modo anónimo a dados económicos/fiscais/territoriais/operacionais e exclui esses fluxos. Decidir compatibilidade semântica antes de alterar a UI/API | `resources/js/lib/simulation-form.ts`, `resources/js/components/simulation-form.tsx`, `StoreSimulationFormDraftRequest.php`, `SimulationFormDrafts.php` | Decisão de produto/privacidade sobre fluxo anónimo e separação de funcionalidades fora da release |
| Fase 5 — motor fiscal | Motor actual executa aritmética genérica introduzida pelo utilizador | AUSENTE para o âmbito fiscal | Não há evidência de cálculos fiscais oficiais de IRPC, IVA, ISPC, IRPS, INSS ou retenções com regras/fontes aprovadas, Decimal, arredondamento fiscal e casos dourados | `app/Services/SimulationExecution.php`, `app/Jobs/RunSimulation.php`, `docs/investmoz/simulations/openapi.json` | Aprovação de regras fiscais e especificação matemática antes de implementação |
| Fase 6 — aduanas, câmbios e calendário | Campos/estrutura parcial de simulação e jobs genéricos | AUSENTE para os cálculos e calendário pedidos | Valor aduaneiro, direitos, ICE, IVA de importação, obrigações cambiais/fiscais e calendário não estão demonstrados como motor completo | app/migrations de simulação e integrações | Fontes e regras oficiais; contratos de moedas/origem e jobs/retries |
| Fase 7 — relatórios e homologação | Snapshot de relatório JSON e hashes existem | PARCIAL | PDF, reconciliação integral API/DB/PDF/JSON, homologação, acessibilidade manual, resiliência e DR não demonstrados | `SimulationReport`, controller/serviço de simulação, testes | Contrato PDF/JSON, casos dourados, acessibilidade e prova de recuperação |
| Fase 8 — release e estabilização | Workflow CI e compose de desenvolvimento existem | PARCIAL | Não há evidência de Go/No-Go fiscal, release imutável, backup/restauro, rollout/rollback ensaiados ou hypercare | `.github/workflows/tests.yml`, `deploy/docker/compose.yaml` | Aprovação de release, owners operacionais e evidência de rollout/rollback |
### Divergências que não serão resolvidas por inferência
1. **Identidade no modo anónimo:** decisão de escopo do utilizador aplica a regra da especificação: o simulador fiscal anónimo pede apenas dados económicos, fiscais, territoriais e operacionais. O formulário actual oferece campos pessoais; a Fase 4 deve separar esses fluxos sem apagar código/dados do portal.
2. **Oportunidades/manifestações:** decisão de escopo do utilizador mantém esses fluxos fora desta release fiscal e preserva código/dados existentes. A Fase 4 deve removê-los da experiência fiscal sem declarar funcionalidades do portal como MVP fiscal.
3. **RLS:** ADR-007 bloqueia activação sem matriz actor/recurso/operação e papéis SQL, enquanto migrations locais contêm policies. A prova de política eficaz, grants e papel runtime permanece BLOQUEADA.
4. **Aprovação normativa:** nenhum cálculo ou taxa fiscal pode ser codificado/publicado com base apenas na fórmula aritmética existente ou em defaults do cliente. Cada regra necessita fonte oficial, período de vigência, aprovador fiscal e revisão jurídica.
5. **Dados/ambientes:** estado actual de migrations e serviços só pode ser confirmado em ambiente de teste isolado; não inferir por CI ou documentação o estado de desenvolvimento/produção.
## RACI funcional proposto
Não foram atribuídos nomes. A nomeação de responsáveis e a aprovação desta matriz são pendências do gate.
| Actividade | Responsável (R) | Accountable (A) | Consultados (C) | Informados (I) |
| --- | --- | --- | --- | --- |
| Aprovar fonte, regra, vigência e casos fiscais | Especialista fiscal | Dono de produto fiscal | Jurídico, dados, engenharia | QA, operações |
| Publicar regra versionada | Engenharia fiscal | Dono de produto fiscal | Fiscal, jurídico, segurança | QA, operações |
| Aprovar privacidade e fluxo anónimo | Privacidade/protecção de dados | Dono de produto | Jurídico, segurança, fiscal | Engenharia, QA |
| Aprovar IAM/RLS/grants | DBA/segurança | Arquitectura de segurança | Engenharia, operações | Dono de produto, QA |
| Homologar cálculos e relatórios | QA fiscal | Dono de produto fiscal | Fiscal, jurídico, engenharia | Operações |
| Autorizar release, rollback e DR | Operações/release | Dono de serviço | DBA, segurança, QA, produto | Stakeholders |
RACI é uma proposta de funções e não atribui autoridade a pessoas. Gate bloqueado até os responsáveis formais serem indicados e aceitarem.
## Backlog e gates de aceitação
| Prioridade | Item | Dependência | Critério de conclusão |
| --- | --- | --- | --- |
| P0 | Resolver divergência do modo anónimo, PII e funcionalidades fora da release | Decisão de produto e privacidade | Formulário e contrato aprovados sem recolha anónima vedada; decisão registada |
| P0 | Aprovar fontes, regras, vigência e arredondamento de cada cálculo | Autoridade fiscal e jurídico | Fonte oficial/hash, regra versionada, casos dourados e dupla revisão registados |
| P0 | Aprovar matriz IAM/RLS e papéis SQL | Segurança/DBA | Testes actor×recurso×operação com papel runtime sem bypass e contexto limpo |
| P1 | Reconciliar OpenAPI integral, migrations e esquema | API/DBA | Validação semântica de contratos e cobertura de operações/campos no CI |
| P1 | Implementar cálculos e obrigações do escopo | Regras aprovadas | Testes críticos 100%, totais aplicáveis ≥95%, Decimal e resultados explicáveis |
| P1 | Aduanas, câmbios, calendário e jobs | Fontes/regras e contrato operacional | Casos dourados, idempotência/retry, reconciliação e dead-letter demonstrados |
| P1 | PDF/JSON e acessibilidade/homologação | Schema de relatório e casos aprovados | API/DB/PDF/JSON reconciliados; teclado/leitor de ecrã/zoom e homologação registados |
| P1 | Preparar release, backup/restauro e rollback | Operações/DBA | Release imutável, backup restaurado em prova, smoke tests e rollback ensaiados |
## Métrica, aprovação e bloqueio
- Go exige 100% dos testes críticos aplicáveis aprovados e pelo menos 95% de todos os testes aplicáveis aprovados. Cobertura de código é métrica separada.
- Ignorados, instáveis, não executados ou sem evidência não contam como aprovados. Um teste aplicável inexistente é lacuna a criar na fase correspondente.
- Não há excepção para falha crítica. Uma falha alta só pode ter excepção formal com impacto, controlo compensatório, responsável, prazo e aprovação; após expiração volta a ser bloqueio.
- É bloqueio de release qualquer cálculo sem fonte aprovada, elegibilidade presumida, inconsistência API/DB/relatórios, fuga de dados, OpenAPI inválido, RLS não demonstrado, migration irreversível ou recuperação/rollback não provados.
## Evidência desta execução
- Branch `codex/simulador-fiscal-phase-0` criada e seleccionada. O working tree tinha alterações locais em `main`; todas foram preservadas e transitam sem commit para a branch. A autoria/intenção de cada alteração anterior não foi auditada nesta fase.
- A execução PHPUnit 12.5.34 com PHP 8.5.11 mais recente, anterior a esta fase, iniciou 15 testes de `SimulationExecutionTest|SimulationFormDraftTest`; os 15 pararam antes das asserções com `Unregistered skills_edu schema exists; reconcile before migration.`. A suite não foi aprovada. O `phpunit.xml` define `DB_DATABASE=investmoz_test`, mas não fixa host/porta/credenciais; esses valores podem vir do `.env`. Como o destino não foi confirmado antes daquela execução, o isolamento da base não pode ser afirmado. Não executar testes que migrem/escrevam até confirmar explicitamente uma base descartável isolada. Nesta Fase 0 não se repetiu a execução nem se inspeccionou `.env`.
- `node` validou parsing dos cinco contratos OpenAPI indicados acima; não é validação semântica.
- Node 24.20.0 e npm 11.19.0 disponíveis. O comando `composer` global e `docker` não foram encontrados no PATH desta execução. Nenhuma dependência foi instalada ou actualizada.
- Nesta Fase 0 não foi iniciada ligação a base de dados ou serviço externo. Não se inspeccionou `.env` nem se usaram credenciais de produção nesta fase. O destino da tentativa PHPUnit anterior é uma pendência de segurança operacional, não uma evidência de ambiente de teste seguro.
## Rollback
Esta fase cria apenas este documento e uma branch Git. Para reverter o artefacto, remover `docs/implementation/simulador-fiscal-governance.md`. A branch pode ser abandonada sem afectar `main`; não apagar/resetar a branch enquanto o working tree contiver alterações locais. Nenhuma migration, dado ou serviço foi alterado.
## Gate da Fase 0
**Estado proposto: APROVADO PARA AVANÇAR À FASE 1** por instrução explícita do utilizador em 2026-10-01. A decisão aprova o escopo e a continuação faseada; não aprova regras/taxas fiscais nem substitui as aprovações fiscais, jurídicas, de segurança e operacionais exigidas para publicação ou release.
**Próxima acção:** executar apenas a Fase 1 com inventário SQL/OpenAPI e validação em ambiente de teste isolado; parar no respectivo gate.
+685
View File
@@ -0,0 +1,685 @@
"TabelaSQL","CampoSQL","TipoPostgreSQL","Nulabilidade","EnumeracoesRestricoes","DescricaoSQL","OrigemMigration","DocumentoOpenAPI","TipoOpenAPI","FormatoOpenAPI","ClassificacaoDados","EstadoMapeamento"
"users","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"users","name","character varying(255)","Não","Não","name","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"users","email","character varying(255)","Não","Não","email","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"users","email_verified_at","timestamp(0) without time zone","Sim","Não","email verified at","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"users","password","character varying(255)","Não","Não","password","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"users","remember_token","character varying(100)","Sim","Não","remember token","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"users","created_at","timestamp(0) without time zone","Sim","Não","Instante de criação","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"users","updated_at","timestamp(0) without time zone","Sim","Não","Instante da última actualização","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"users","two_factor_secret","text","Sim","Não","two factor secret","database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"users","two_factor_recovery_codes","text","Sim","Não","two factor recovery codes","database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"users","two_factor_confirmed_at","timestamp(0) without time zone","Sim","Não","two factor confirmed at","database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"password_reset_tokens","email","character varying(255)","Não","Não","email","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"password_reset_tokens","token","character varying(255)","Não","Não","token","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"password_reset_tokens","created_at","timestamp(0) without time zone","Sim","Não","Instante de criação","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sessions","id","character varying(255)","Não","Não","Identificador primário da entidade","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sessions","user_id","bigint","Sim","Não","user id","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sessions","ip_address","character varying(45)","Sim","Não","ip address","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sessions","user_agent","text","Sim","Não","user agent","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sessions","payload","text","Não","Não","Conteúdo da submissão/evento; contrato por tipo pendente","database/migrations/0001_01_01_000000_create_users_table.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload","|object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sessions","last_activity","integer","Não","Não","last activity","database/migrations/0001_01_01_000000_create_users_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cache","key","character varying(255)","Não","Não","key","database/migrations/0001_01_01_000001_create_cache_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cache","value","text","Não","Não","value","database/migrations/0001_01_01_000001_create_cache_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cache","expiration","bigint","Não","Não","expiration","database/migrations/0001_01_01_000001_create_cache_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cache_locks","key","character varying(255)","Não","Não","key","database/migrations/0001_01_01_000001_create_cache_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cache_locks","owner","character varying(255)","Não","Não","owner","database/migrations/0001_01_01_000001_create_cache_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cache_locks","expiration","bigint","Não","Não","expiration","database/migrations/0001_01_01_000001_create_cache_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"jobs","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"jobs","queue","character varying(255)","Não","Não","queue","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"jobs","payload","text","Não","Não","Conteúdo da submissão/evento; contrato por tipo pendente","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload","|object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"jobs","attempts","smallint","Não","Não","attempts","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"jobs","reserved_at","integer","Sim","Não","reserved at","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"jobs","available_at","integer","Não","Não","available at","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"jobs","created_at","integer","Não","Não","Instante de criação","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","id","character varying(255)","Não","Não","Identificador primário da entidade","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"job_batches","name","character varying(255)","Não","Não","name","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"job_batches","total_jobs","integer","Não","Não","total jobs","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","pending_jobs","integer","Não","Não","pending jobs","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","failed_jobs","integer","Não","Não","failed jobs","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","failed_job_ids","text","Não","Não","failed job ids","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","options","text","Sim","Não","options","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\unification\openapi.json#/components/schemas/References/properties/options","array","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"job_batches","cancelled_at","integer","Sim","Não","cancelled at","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","created_at","integer","Não","Não","Instante de criação","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"job_batches","finished_at","integer","Sim","Não","finished at","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"failed_jobs","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"failed_jobs","uuid","character varying(255)","Não","Não","uuid","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"failed_jobs","connection","character varying(255)","Não","Não","connection","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"failed_jobs","queue","character varying(255)","Não","Não","queue","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"failed_jobs","payload","text","Não","Não","Conteúdo da submissão/evento; contrato por tipo pendente","database/migrations/0001_01_01_000002_create_jobs_table.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload","|object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"failed_jobs","exception","text","Não","Não","exception","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"failed_jobs","failed_at","timestamp(0) without time zone","Não","Não","failed at","database/migrations/0001_01_01_000002_create_jobs_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"passkeys","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/2024_01_01_000000_create_passkeys_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"passkeys","user_id","bigint","Não","Não","user id","database/migrations/2024_01_01_000000_create_passkeys_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"passkeys","name","character varying(255)","Não","Não","name","database/migrations/2024_01_01_000000_create_passkeys_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"passkeys","credential_id","character varying(255)","Não","Não","credential id","database/migrations/2024_01_01_000000_create_passkeys_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"passkeys","credential","json","Não","Não","credential","database/migrations/2024_01_01_000000_create_passkeys_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"passkeys","last_used_at","timestamp(0) without time zone","Sim","Não","last used at","database/migrations/2024_01_01_000000_create_passkeys_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"passkeys","created_at","timestamp(0) without time zone","Sim","Não","Instante de criação","database/migrations/2024_01_01_000000_create_passkeys_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"passkeys","updated_at","timestamp(0) without time zone","Sim","Não","Instante da última actualização","database/migrations/2024_01_01_000000_create_passkeys_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/2026_01_01_000001_create_sectors_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","name","character varying(150)","Não","Não","name","database/migrations/2026_01_01_000001_create_sectors_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","slug","character varying(180)","Não","Não","slug","database/migrations/2026_01_01_000001_create_sectors_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/slug","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","short_description","character varying(255)","Sim","Não","short description","database/migrations/2026_01_01_000001_create_sectors_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sectors","description","text","Sim","Não","description","database/migrations/2026_01_01_000001_create_sectors_table.php","docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","icon","character varying(100)","Sim","Não","icon","database/migrations/2026_01_01_000001_create_sectors_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sectors","sort_order","integer","Não","Não","sort order","database/migrations/2026_01_01_000001_create_sectors_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/sort_order","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","is_active","boolean","Não","Não","is active","database/migrations/2026_01_01_000001_create_sectors_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sectors","published_at","timestamp(0) without time zone","Sim","Não","published at","database/migrations/2026_01_01_000001_create_sectors_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sectors","created_at","timestamp(0) without time zone","Sim","Não","Instante de criação","database/migrations/2026_01_01_000001_create_sectors_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sectors","updated_at","timestamp(0) without time zone","Sim","Não","Instante da última actualização","database/migrations/2026_01_01_000001_create_sectors_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sectors","deleted_at","timestamp(0) without time zone","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_01_01_000001_create_sectors_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","sector_id","bigint","Não","Não","sector id","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/sector_id","integer|null","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","reference","character varying(80)","Não","Não","reference","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/reference","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","title","character varying(200)","Não","Não","title","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","slug","character varying(220)","Não","Não","slug","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/slug","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","summary","character varying(500)","Sim","Não","summary","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/summary","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","description","text","Sim","Não","description","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","province","character varying(100)","Sim","Não","province","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/province ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/province","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","district","character varying(100)","Sim","Não","district","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/district ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/district","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","promoter_name","character varying(200)","Sim","Não","promoter name","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","promoter_type","character varying(100)","Sim","Não","promoter type","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","maturity_level","smallint","Sim","Não","maturity level","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","maturity_label","character varying(120)","Sim","Não","maturity label","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/maturity_label","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","estimated_investment_amount","numeric(18,2)","Sim","Não","estimated investment amount","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/estimated_investment_amount","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","estimated_investment_currency","character varying(10)","Não","Não","estimated investment currency","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/estimated_investment_currency","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","financing_modality","character varying(120)","Sim","Não","financing modality","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","expected_impact","text","Sim","Não","expected impact","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","status","character varying(50)","Não","Não","status","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","is_featured","boolean","Não","Não","is featured","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","published_at","timestamp(0) without time zone","Sim","Não","published at","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","last_validated_at","timestamp(0) without time zone","Sim","Não","last validated at","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","created_at","timestamp(0) without time zone","Sim","Não","Instante de criação","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"opportunities","updated_at","timestamp(0) without time zone","Sim","Não","Instante da última actualização","database/migrations/2026_01_01_000002_create_opportunities_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"opportunities","deleted_at","timestamp(0) without time zone","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_01_01_000002_create_opportunities_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","id","bigint","Não","Não","Identificador primário da entidade","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","opportunity_id","bigint","Sim","Não","opportunity id","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","reference","character varying(80)","Não","Não","reference","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/reference","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","full_name","character varying(150)","Não","Não","full name","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/full_name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","email","character varying(150)","Não","Não","email","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","phone","character varying(30)","Sim","Não","phone","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/phone","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","investor_type","character varying(80)","Não","Não","investor type","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","country","character varying(100)","Não","Não","country","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/country","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","sector_interest","character varying(150)","Não","Não","sector interest","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","opportunity_reference","character varying(80)","Sim","Não","opportunity reference","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","investment_range","character varying(80)","Sim","Não","investment range","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","message","text","Não","Não","message","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\unification\openapi.json#/components/schemas/Error/properties/message","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","privacy_consent","boolean","Não","Não","privacy consent","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","consented_at","timestamp(0) without time zone","Sim","Não","consented at","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","status","character varying(50)","Não","Não","status","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","source_page","character varying(255)","Sim","Não","source page","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","ip_address","inet","Sim","Não","ip address","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","user_agent","text","Sim","Não","user agent","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","metadata","json","Sim","Não","Metadados flexíveis; schema aplicacional pendente","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","reviewed_at","timestamp(0) without time zone","Sim","Não","reviewed at","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","reviewed_by","bigint","Sim","Não","reviewed by","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","created_at","timestamp(0) without time zone","Sim","Não","Instante de criação","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"interest_requests","updated_at","timestamp(0) without time zone","Sim","Não","Instante da última actualização","database/migrations/2026_01_01_000003_create_interest_requests_table.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"interest_requests","deleted_at","timestamp(0) without time zone","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_01_01_000003_create_interest_requests_table.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"organizations","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"organizations","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"organizations","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"organizations","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"organizations","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"organizations","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"organizations","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"organizations","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"organizations","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"organizations","name","text","Não","Não","name","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"organizations","organization_type","text","Não","organization_type IN ('public','private','ngo','education')","organization type","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"memberships","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"memberships","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"memberships","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"memberships","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","user_id","bigint","Não","Não","user id","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","organization_id","uuid","Não","Não","organization id","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"memberships","role","text","Não","role IN ('admin','manager','officer','viewer')","role","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/role ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/role","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","user_id","bigint","Sim","Não","user id","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","subject_ref","text","Não","Não","subject ref","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","purpose","text","Não","Não","purpose","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\zones\openapi.json#/components/schemas/Geometry/properties/purpose","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","status","text","Não","status IN ('granted','withdrawn','expired')","status","database/migrations/2026_09_24_001000_investmoz_identity.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"consents","granted_at","timestamptz","Sim","Não","granted at","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"consents","withdrawn_at","timestamptz","Sim","Não","withdrawn at","database/migrations/2026_09_24_001000_investmoz_identity.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_002000_investmoz_content.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"documents","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_002000_investmoz_content.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"documents","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_002000_investmoz_content.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"documents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_002000_investmoz_content.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"documents","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","file_name","text","Não","Não","file name","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","media_type","text","Não","Não","media type","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","storage_key","text","Não","Não","Chave única no armazenamento","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","sha256","char(64)","Não","Não","SHA-256 hexadecimal; 64 caracteres minúsculos","database/migrations/2026_09_24_002000_investmoz_content.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"documents","status","text","Não","status IN ('active','quarantined','deleted')","status","database/migrations/2026_09_24_002000_investmoz_content.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_locations","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_locations","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_locations","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_locations","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_locations","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","name","text","Não","Não","name","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_locations","location_type","text","Não","location_type IN ('country','province','district','municipality','site')","location type","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","parent_id","uuid","Sim","Não","parent id","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_locations","code","text","Não","Não","code","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_geometries","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_geometries","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_geometries","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_geometries","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_003000_investmoz_geography.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"geo_geometries","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","geo_location_id","uuid","Não","Não","geo location id","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","geometry_type","text","Não","geometry_type IN ('point','line','polygon','multipolygon')","Tipo declarado coerente com GeometryType(geom)","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","geom","geometry(Geometry,4326)","Não","Não","Geometria 2D WGS84 SRID 4326, válida e não vazia","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"geo_geometries","properties","jsonb","Não","Não","Propriedades geográficas; schema aplicacional pendente","database/migrations/2026_09_24_003000_investmoz_geography.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","code","text","Não","Não","code","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","name","text","Não","Não","name","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","status","text","Não","status IN ('draft','planned','active','suspended','completed','cancelled')","status","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","description","text","Sim","Não","description","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","budget_amount","numeric(20,2)","Sim","Não","Orçamento exacto não negativo","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","budget_currency","char(3)","Sim","Não","Código monetário do orçamento; lista ISO autorizada pendente","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","start_date","date","Sim","Não","start date","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"projects","end_date","date","Sim","Não","end date","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/end_date ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/end_date","string|null","date","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"projects","metadata","jsonb","Não","Não","Metadados flexíveis; schema aplicacional pendente","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_locations","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_locations","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_locations","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_locations","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","project_id","uuid","Não","Não","project id","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_locations","geo_location_id","uuid","Não","Não","geo location id","database/migrations/2026_09_24_004000_investmoz_projects.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_locations","role","text","Não","role IN ('primary','secondary','impact')","role","database/migrations/2026_09_24_004000_investmoz_projects.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/role ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/role","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"economic_clusters","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"economic_clusters","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"economic_clusters","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"economic_clusters","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"economic_clusters","code","text","Não","Não","code","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","name","text","Não","Não","name","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","sector","text","Não","Não","sector","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/sector ; docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/sector","object|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","status","text","Não","status IN ('planned','active','inactive')","status","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"economic_clusters","metadata","jsonb","Não","Não","Metadados flexíveis; schema aplicacional pendente","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chains","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chains","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chains","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chains","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chains","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chains","cluster_id","uuid","Não","Não","cluster id","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","code","text","Não","Não","code","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","name","text","Não","Não","name","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chains","stage","text","Não","stage IN ('input','production','processing','logistics','market')","stage","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chain_links","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chain_links","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chain_links","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"value_chain_links","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","from_value_chain_id","uuid","Não","Não","from value chain id","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","to_value_chain_id","uuid","Não","Não","to value chain id","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"value_chain_links","link_type","text","Não","link_type IN ('supplies','processes','distributes','supports')","link type","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cluster_projects","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"cluster_projects","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cluster_projects","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cluster_projects","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"cluster_projects","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cluster_projects","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"cluster_projects","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cluster_projects","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"cluster_projects","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"cluster_projects","cluster_id","uuid","Não","Não","cluster id","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"cluster_projects","project_id","uuid","Não","Não","project id","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"cluster_projects","role","text","Não","role IN ('anchor','complementary','supporting')","role","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/role ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/role","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"anchor_project_links","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"anchor_project_links","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"anchor_project_links","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"anchor_project_links","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"anchor_project_links","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"anchor_project_links","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"anchor_project_links","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"anchor_project_links","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"anchor_project_links","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"anchor_project_links","anchor_project_id","uuid","Não","Não","anchor project id","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"anchor_project_links","cluster_id","uuid","Não","Não","cluster id","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"anchor_project_links","rationale","text","Não","Não","rationale","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"complementary_project_links","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"complementary_project_links","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"complementary_project_links","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_005000_investmoz_clusters.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"complementary_project_links","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","anchor_project_id","uuid","Não","Não","anchor project id","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","complementary_project_id","uuid","Não","Não","complementary project id","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"complementary_project_links","relation_type","text","Não","relation_type IN ('dependency','enabler','supplier','market')","relation type","database/migrations/2026_09_24_005000_investmoz_clusters.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"development_corridors","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"development_corridors","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"development_corridors","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"development_corridors","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"development_corridors","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"development_corridors","code","text","Não","Não","code","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","name","text","Não","Não","name","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","status","text","Não","status IN ('proposed','active','closed')","status","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"development_corridors","description","text","Sim","Não","description","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_locations","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_locations","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_locations","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_locations","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_locations","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_locations","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_locations","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_locations","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_locations","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_locations","corridor_id","uuid","Não","Não","corridor id","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/corridor_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/corridor_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/corridor_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_locations","geo_location_id","uuid","Não","Não","geo location id","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_locations","sequence_no","integer","Não","Não","sequence no","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_projects","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_projects","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_projects","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_projects","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_projects","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_projects","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_projects","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_projects","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_projects","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_projects","corridor_id","uuid","Não","Não","corridor id","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/corridor_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/corridor_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/corridor_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_projects","project_id","uuid","Não","Não","project id","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_clusters","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_clusters","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_clusters","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_clusters","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_clusters","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_clusters","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_clusters","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_clusters","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_clusters","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_006000_investmoz_corridors.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"corridor_clusters","corridor_id","uuid","Não","Não","corridor id","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/corridor_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/corridor_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/corridor_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"corridor_clusters","cluster_id","uuid","Não","Não","cluster id","database/migrations/2026_09_24_006000_investmoz_corridors.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investors","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investors","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investors","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investors","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investors","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","organization_id","uuid","Sim","Não","organization id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","legal_name","text","Não","Não","legal name","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","investor_type","text","Não","investor_type IN ('domestic','foreign','institutional','individual')","investor type","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investors","status","text","Não","status IN ('prospect','active','inactive')","status","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"contacts","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"contacts","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"contacts","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"contacts","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"contacts","full_name","text","Não","Não","full name","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/full_name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","email","text","Sim","Não","email","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"contacts","phone","text","Sim","Não","phone","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/phone","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_contacts","investor_id","uuid","Não","Não","investor id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_contacts","contact_id","uuid","Não","Não","contact id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_contacts","is_primary","boolean","Não","Não","Contacto principal deste investidor; limite quantitativo pendente","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/is_primary ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/is_primary","boolean","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_contacts","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_contacts","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","investor_id","uuid","Sim","Não","investor id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","owner_user_id","bigint","Sim","Não","owner user id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"leads","title","text","Não","Não","title","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","status","text","Não","status IN ('new','qualified','disqualified','converted')","status","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","estimated_amount","numeric(20,2)","Sim","Não","Montante estimado exacto não negativo","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/estimated_amount","number","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"leads","currency","char(3)","Sim","Não","Código monetário; três letras maiúsculas; lista ISO autorizada pendente","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/currency","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","lead_id","uuid","Sim","Não","lead id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","project_id","uuid","Sim","Não","project id","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","owner_user_id","bigint","Sim","Não","owner user id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","stage","text","Não","stage IN ('discovery','assessment','negotiation','approved','lost')","stage","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","amount","numeric(20,2)","Sim","Não","Montante monetário exacto não negativo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"crm_opportunities","currency","char(3)","Sim","Não","Código monetário; três letras maiúsculas; lista ISO autorizada pendente","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/currency","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"crm_opportunities","probability","numeric(5,2)","Sim","Não","probability","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"activities","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"activities","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"activities","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"activities","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","investor_id","uuid","Sim","Não","investor id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","opportunity_id","uuid","Sim","Não","opportunity id","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"activities","actor_user_id","bigint","Sim","Não","actor user id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","activity_type","text","Não","activity_type IN ('call','meeting','email','note')","activity type","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","occurred_at","timestamptz","Não","Não","occurred at","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"activities","summary","text","Não","Não","summary","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/summary","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"tasks","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"tasks","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"tasks","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"tasks","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"tasks","assignee_user_id","bigint","Sim","Não","assignee user id","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"tasks","opportunity_id","uuid","Sim","Não","opportunity id","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","title","text","Não","Não","title","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","status","text","Não","status IN ('open','in_progress','done','cancelled')","status","database/migrations/2026_09_24_007000_investmoz_crm.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"tasks","due_at","timestamptz","Sim","Não","due at","database/migrations/2026_09_24_007000_investmoz_crm.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_journeys","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_journeys","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_journeys","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_journeys","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","investor_id","uuid","Não","Não","investor id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_journeys","opportunity_id","uuid","Sim","Não","opportunity id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_journeys","status","text","Não","status IN ('started','active','completed','cancelled')","status","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"investor_journeys","started_at","timestamptz","Não","Não","started at","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"journey_steps","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"journey_steps","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"journey_steps","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"journey_steps","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","journey_id","uuid","Não","Não","journey id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","step_code","text","Não","Não","step code","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","sequence_no","integer","Não","Não","sequence no","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_steps","status","text","Não","status IN ('pending','active','completed','blocked')","status","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","journey_step_id","uuid","Não","Não","journey step id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","submitted_by","bigint","Sim","Não","submitted by","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"submissions","status","text","Não","status IN ('draft','submitted','accepted','rejected')","status","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","payload","jsonb","Não","Não","Conteúdo da submissão/evento; contrato por tipo pendente","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload","|object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"submissions","submitted_at","timestamptz","Sim","Não","submitted at","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"aftercare_plans","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"aftercare_plans","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"aftercare_plans","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"aftercare_plans","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","investor_id","uuid","Não","Não","investor id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","project_id","uuid","Sim","Não","project id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"aftercare_plans","owner_user_id","bigint","Sim","Não","owner user id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_plans","status","text","Não","status IN ('draft','active','closed')","status","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"aftercare_plans","review_date","date","Sim","Não","review date","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"constraints","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"constraints","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"constraints","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"constraints","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","aftercare_plan_id","uuid","Não","Não","aftercare plan id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","category","text","Não","Não","category","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/category","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"constraints","description","text","Não","Não","description","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"constraints","severity","text","Não","severity IN ('low','medium','high','critical')","severity","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"constraints","status","text","Não","status IN ('open','mitigating','resolved')","status","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"escalations","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"escalations","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"escalations","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"escalations","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"escalations","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","constraint_id","uuid","Não","Não","constraint id","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","escalated_by","bigint","Sim","Não","escalated by","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","level","text","Não","level IN ('operational','management','executive')","level","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"escalations","status","text","Não","status IN ('open','acknowledged','closed')","status","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"escalations","escalated_at","timestamptz","Não","Não","escalated at","database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"skills","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"skills","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"skills","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"skills","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"skills","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"skills","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"skills","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"skills","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"skills","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"skills","code","text","Não","Não","code","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"skills","name","text","Não","Não","name","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"occupations","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"occupations","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"occupations","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"occupations","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"occupations","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"occupations","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"occupations","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"occupations","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"occupations","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"occupations","code","text","Não","Não","code","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"occupations","name","text","Não","Não","name","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_needs","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_needs","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_needs","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_needs","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_needs","project_id","uuid","Sim","Não","project id","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","occupation_id","uuid","Sim","Não","occupation id","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/occupation_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/occupation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","skill_id","uuid","Sim","Não","skill id","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_needs","quantity","integer","Não","Não","quantity","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/quantity ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/quantity ; docs\investmoz\unification\openapi.json#/components/schemas/Need/properties/quantity","integer|number","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_needs","priority","text","Não","priority IN ('Must','Should','Could')","priority","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/priority ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/priority","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_providers","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_providers","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_providers","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_providers","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_providers","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_providers","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_providers","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_providers","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_providers","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_providers","organization_id","uuid","Sim","Não","organization id","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"training_providers","name","text","Não","Não","name","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"training_providers","status","text","Não","status IN ('candidate','approved','inactive')","status","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"courses","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"courses","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"courses","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"courses","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"courses","provider_id","uuid","Não","Não","provider id","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"courses","code","text","Não","Não","code","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","title","text","Não","Não","title","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title","string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"courses","status","text","Não","status IN ('draft','published','retired')","status","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"education_opportunities","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"education_opportunities","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"education_opportunities","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"education_opportunities","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"education_opportunities","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","course_id","uuid","Não","Não","course id","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","geo_location_id","uuid","Sim","Não","geo location id","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","start_date","date","Não","Não","start date","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","end_date","date","Sim","Não","end date","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/end_date ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/end_date","string|null","date","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"education_opportunities","capacity","integer","Sim","Não","capacity","database/migrations/2026_09_24_009000_investmoz_education.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_opportunities","status","text","Não","status IN ('planned','open','closed','cancelled')","status","database/migrations/2026_09_24_009000_investmoz_education.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_documents","project_id","uuid","Não","Não","project id","database/migrations/2026_09_24_009500_investmoz_document_links.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"project_documents","document_id","uuid","Não","Não","document id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"project_documents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_documents","investor_id","uuid","Não","Não","investor id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_documents","document_id","uuid","Não","Não","document id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"investor_documents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_documents","journey_id","uuid","Não","Não","journey id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_documents","document_id","uuid","Não","Não","document id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"journey_documents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_documents","aftercare_plan_id","uuid","Não","Não","aftercare plan id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_documents","document_id","uuid","Não","Não","document id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"aftercare_documents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_documents","education_opportunity_id","uuid","Não","Não","education opportunity id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_documents","document_id","uuid","Não","Não","document id","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"education_documents","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_009500_investmoz_document_links.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"audit_events","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"audit_events","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","actor_user_id","bigint","Sim","Não","actor user id","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","entity_type","text","Não","Não","entity type","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","entity_id","uuid","Sim","Não","ID histórico UUID sem FK; a entidade pode já não existir","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","action","text","Não","Não","action","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","occurred_at","timestamptz","Não","Não","occurred at","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","before_data","jsonb","Sim","Não","Representação anterior; aplicação deve remover segredos","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"audit_events","after_data","jsonb","Sim","Não","Representação posterior; aplicação deve remover segredos","database/migrations/2026_09_24_010000_investmoz_governance_ai.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"outbox_events","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"outbox_events","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"outbox_events","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"outbox_events","aggregate_type","text","Não","Não","aggregate type","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","aggregate_id","uuid","Não","Não","UUID do agregado produtor; sem FK polimórfica","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","event_type","text","Não","Não","event type","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","payload","jsonb","Não","Não","Conteúdo da submissão/evento; contrato por tipo pendente","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload","|object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"outbox_events","status","text","Não","status IN ('pending','published','failed')","status","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"outbox_events","available_at","timestamptz","Não","Não","available at","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"outbox_events","published_at","timestamptz","Sim","Não","published at","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"inbox_events","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"inbox_events","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"inbox_events","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"inbox_events","message_id","text","Não","Não","message id","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","event_type","text","Não","Não","event type","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","payload","jsonb","Não","Não","Conteúdo da submissão/evento; contrato por tipo pendente","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload","|object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"inbox_events","status","text","Não","status IN ('received','processed','failed')","status","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"inbox_events","received_at","timestamptz","Não","Não","received at","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"inbox_events","processed_at","timestamptz","Sim","Não","processed at","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sync_runs","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sync_runs","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sync_runs","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sync_runs","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","integration_name","text","Não","Não","integration name","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","direction","text","Não","direction IN ('inbound','outbound')","direction","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","status","text","Não","status IN ('running','succeeded','failed','cancelled')","status","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"sync_runs","started_at","timestamptz","Não","Não","started at","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","ended_at","timestamptz","Sim","Não","ended at","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"sync_runs","totals","jsonb","Não","Não","totals","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","id","uuid","Não","Não","Identificador primário da entidade","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id","integer|string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"reconciliation_issues","source_system","text","Não","Não","Sistema de origem; integra a chave estável de importação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","external_id","text","Sim","Não","Identificador na origem; NULL quando não fornecido","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","version_no","integer","Não","Não","Versão positiva; comparação compete ao importador","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no","integer","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"reconciliation_issues","data_classification","text","Não","data_classification IN ('public','internal','confidential','restricted')","Classificação de acesso; não substitui autorização","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","correlation_id","uuid","Sim","Não","Correlação entre operações; não é FK","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id","string","uuid","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"reconciliation_issues","created_at","timestamptz","Não","Não","Instante de criação","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","updated_at","timestamptz","Não","Não","Instante da última actualização","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at","string","date-time","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"reconciliation_issues","deleted_at","timestamptz","Sim","Não","Eliminação lógica; NULL indica activo","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","sync_run_id","uuid","Não","Não","sync run id","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","entity_type","text","Não","Não","entity type","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","external_id_ref","text","Sim","Não","external id ref","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","issue_type","text","Não","issue_type IN ('missing','duplicate','orphan','checksum','geometry','value')","issue type","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","severity","text","Não","severity IN ('warning','error')","severity","database/migrations/2026_09_24_011000_investmoz_integrations.php","","","","NAO_REGISTADA_NO_DICIONARIO","SEM_CAMPO_OPENAPI_DOCUMENTADO"
"reconciliation_issues","status","text","Não","status IN ('open','accepted','resolved')","status","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status","|integer|string","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
"reconciliation_issues","details","jsonb","Não","Não","details","database/migrations/2026_09_24_011000_investmoz_integrations.php","docs\investmoz\unification\openapi.json#/components/schemas/Need/properties/details","object","","NAO_REGISTADA_NO_DICIONARIO","NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA"
1 TabelaSQL CampoSQL TipoPostgreSQL Nulabilidade EnumeracoesRestricoes DescricaoSQL OrigemMigration DocumentoOpenAPI TipoOpenAPI FormatoOpenAPI ClassificacaoDados EstadoMapeamento
2 users id bigint Não Não Identificador primário da entidade database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
3 users name character varying(255) Não Não name database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
4 users email character varying(255) Não Não email database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
5 users email_verified_at timestamp(0) without time zone Sim Não email verified at database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
6 users password character varying(255) Não Não password database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
7 users remember_token character varying(100) Sim Não remember token database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
8 users created_at timestamp(0) without time zone Sim Não Instante de criação database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
9 users updated_at timestamp(0) without time zone Sim Não Instante da última actualização database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
10 users two_factor_secret text Sim Não two factor secret database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
11 users two_factor_recovery_codes text Sim Não two factor recovery codes database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
12 users two_factor_confirmed_at timestamp(0) without time zone Sim Não two factor confirmed at database/migrations/2025_08_14_170933_add_two_factor_columns_to_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
13 password_reset_tokens email character varying(255) Não Não email database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
14 password_reset_tokens token character varying(255) Não Não token database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
15 password_reset_tokens created_at timestamp(0) without time zone Sim Não Instante de criação database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
16 sessions id character varying(255) Não Não Identificador primário da entidade database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
17 sessions user_id bigint Sim Não user id database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
18 sessions ip_address character varying(45) Sim Não ip address database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
19 sessions user_agent text Sim Não user agent database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
20 sessions payload text Não Não Conteúdo da submissão/evento; contrato por tipo pendente database/migrations/0001_01_01_000000_create_users_table.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload |object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
21 sessions last_activity integer Não Não last activity database/migrations/0001_01_01_000000_create_users_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
22 cache key character varying(255) Não Não key database/migrations/0001_01_01_000001_create_cache_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
23 cache value text Não Não value database/migrations/0001_01_01_000001_create_cache_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
24 cache expiration bigint Não Não expiration database/migrations/0001_01_01_000001_create_cache_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
25 cache_locks key character varying(255) Não Não key database/migrations/0001_01_01_000001_create_cache_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
26 cache_locks owner character varying(255) Não Não owner database/migrations/0001_01_01_000001_create_cache_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
27 cache_locks expiration bigint Não Não expiration database/migrations/0001_01_01_000001_create_cache_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
28 jobs id bigint Não Não Identificador primário da entidade database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
29 jobs queue character varying(255) Não Não queue database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
30 jobs payload text Não Não Conteúdo da submissão/evento; contrato por tipo pendente database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload |object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
31 jobs attempts smallint Não Não attempts database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
32 jobs reserved_at integer Sim Não reserved at database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
33 jobs available_at integer Não Não available at database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
34 jobs created_at integer Não Não Instante de criação database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
35 job_batches id character varying(255) Não Não Identificador primário da entidade database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
36 job_batches name character varying(255) Não Não name database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
37 job_batches total_jobs integer Não Não total jobs database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
38 job_batches pending_jobs integer Não Não pending jobs database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
39 job_batches failed_jobs integer Não Não failed jobs database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
40 job_batches failed_job_ids text Não Não failed job ids database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
41 job_batches options text Sim Não options database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\unification\openapi.json#/components/schemas/References/properties/options array NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
42 job_batches cancelled_at integer Sim Não cancelled at database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
43 job_batches created_at integer Não Não Instante de criação database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
44 job_batches finished_at integer Sim Não finished at database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
45 failed_jobs id bigint Não Não Identificador primário da entidade database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
46 failed_jobs uuid character varying(255) Não Não uuid database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
47 failed_jobs connection character varying(255) Não Não connection database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
48 failed_jobs queue character varying(255) Não Não queue database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
49 failed_jobs payload text Não Não Conteúdo da submissão/evento; contrato por tipo pendente database/migrations/0001_01_01_000002_create_jobs_table.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload |object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
50 failed_jobs exception text Não Não exception database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
51 failed_jobs failed_at timestamp(0) without time zone Não Não failed at database/migrations/0001_01_01_000002_create_jobs_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
52 passkeys id bigint Não Não Identificador primário da entidade database/migrations/2024_01_01_000000_create_passkeys_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
53 passkeys user_id bigint Não Não user id database/migrations/2024_01_01_000000_create_passkeys_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
54 passkeys name character varying(255) Não Não name database/migrations/2024_01_01_000000_create_passkeys_table.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
55 passkeys credential_id character varying(255) Não Não credential id database/migrations/2024_01_01_000000_create_passkeys_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
56 passkeys credential json Não Não credential database/migrations/2024_01_01_000000_create_passkeys_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
57 passkeys last_used_at timestamp(0) without time zone Sim Não last used at database/migrations/2024_01_01_000000_create_passkeys_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
58 passkeys created_at timestamp(0) without time zone Sim Não Instante de criação database/migrations/2024_01_01_000000_create_passkeys_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
59 passkeys updated_at timestamp(0) without time zone Sim Não Instante da última actualização database/migrations/2024_01_01_000000_create_passkeys_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
60 sectors id bigint Não Não Identificador primário da entidade database/migrations/2026_01_01_000001_create_sectors_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
61 sectors name character varying(150) Não Não name database/migrations/2026_01_01_000001_create_sectors_table.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
62 sectors slug character varying(180) Não Não slug database/migrations/2026_01_01_000001_create_sectors_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/slug string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
63 sectors short_description character varying(255) Sim Não short description database/migrations/2026_01_01_000001_create_sectors_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
64 sectors description text Sim Não description database/migrations/2026_01_01_000001_create_sectors_table.php docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
65 sectors icon character varying(100) Sim Não icon database/migrations/2026_01_01_000001_create_sectors_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
66 sectors sort_order integer Não Não sort order database/migrations/2026_01_01_000001_create_sectors_table.php docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/sort_order integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
67 sectors is_active boolean Não Não is active database/migrations/2026_01_01_000001_create_sectors_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
68 sectors published_at timestamp(0) without time zone Sim Não published at database/migrations/2026_01_01_000001_create_sectors_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
69 sectors created_at timestamp(0) without time zone Sim Não Instante de criação database/migrations/2026_01_01_000001_create_sectors_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
70 sectors updated_at timestamp(0) without time zone Sim Não Instante da última actualização database/migrations/2026_01_01_000001_create_sectors_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
71 sectors deleted_at timestamp(0) without time zone Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_01_01_000001_create_sectors_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
72 opportunities id bigint Não Não Identificador primário da entidade database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
73 opportunities sector_id bigint Não Não sector id database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/sector_id integer|null NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
74 opportunities reference character varying(80) Não Não reference database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/reference string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
75 opportunities title character varying(200) Não Não title database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
76 opportunities slug character varying(220) Não Não slug database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/slug string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
77 opportunities summary character varying(500) Sim Não summary database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/summary string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
78 opportunities description text Sim Não description database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
79 opportunities province character varying(100) Sim Não province database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/province ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/province string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
80 opportunities district character varying(100) Sim Não district database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/district ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/district string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
81 opportunities promoter_name character varying(200) Sim Não promoter name database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
82 opportunities promoter_type character varying(100) Sim Não promoter type database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
83 opportunities maturity_level smallint Sim Não maturity level database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
84 opportunities maturity_label character varying(120) Sim Não maturity label database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/maturity_label string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
85 opportunities estimated_investment_amount numeric(18,2) Sim Não estimated investment amount database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/estimated_investment_amount string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
86 opportunities estimated_investment_currency character varying(10) Não Não estimated investment currency database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/estimated_investment_currency string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
87 opportunities financing_modality character varying(120) Sim Não financing modality database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
88 opportunities expected_impact text Sim Não expected impact database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
89 opportunities status character varying(50) Não Não status database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
90 opportunities is_featured boolean Não Não is featured database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
91 opportunities published_at timestamp(0) without time zone Sim Não published at database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
92 opportunities last_validated_at timestamp(0) without time zone Sim Não last validated at database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
93 opportunities created_at timestamp(0) without time zone Sim Não Instante de criação database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
94 opportunities updated_at timestamp(0) without time zone Sim Não Instante da última actualização database/migrations/2026_01_01_000002_create_opportunities_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
95 opportunities deleted_at timestamp(0) without time zone Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_01_01_000002_create_opportunities_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
96 interest_requests id bigint Não Não Identificador primário da entidade database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
97 interest_requests opportunity_id bigint Sim Não opportunity id database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
98 interest_requests reference character varying(80) Não Não reference database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/reference string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
99 interest_requests full_name character varying(150) Não Não full name database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/full_name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
100 interest_requests email character varying(150) Não Não email database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
101 interest_requests phone character varying(30) Sim Não phone database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/phone string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
102 interest_requests investor_type character varying(80) Não Não investor type database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
103 interest_requests country character varying(100) Não Não country database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/country string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
104 interest_requests sector_interest character varying(150) Não Não sector interest database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
105 interest_requests opportunity_reference character varying(80) Sim Não opportunity reference database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
106 interest_requests investment_range character varying(80) Sim Não investment range database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
107 interest_requests message text Não Não message database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\unification\openapi.json#/components/schemas/Error/properties/message string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
108 interest_requests privacy_consent boolean Não Não privacy consent database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
109 interest_requests consented_at timestamp(0) without time zone Sim Não consented at database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
110 interest_requests status character varying(50) Não Não status database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
111 interest_requests source_page character varying(255) Sim Não source page database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
112 interest_requests ip_address inet Sim Não ip address database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
113 interest_requests user_agent text Sim Não user agent database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
114 interest_requests metadata json Sim Não Metadados flexíveis; schema aplicacional pendente database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
115 interest_requests reviewed_at timestamp(0) without time zone Sim Não reviewed at database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
116 interest_requests reviewed_by bigint Sim Não reviewed by database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
117 interest_requests created_at timestamp(0) without time zone Sim Não Instante de criação database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
118 interest_requests updated_at timestamp(0) without time zone Sim Não Instante da última actualização database/migrations/2026_01_01_000003_create_interest_requests_table.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
119 interest_requests deleted_at timestamp(0) without time zone Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_01_01_000003_create_interest_requests_table.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
120 organizations id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
121 organizations source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
122 organizations external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
123 organizations version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
124 organizations data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
125 organizations correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
126 organizations created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
127 organizations updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
128 organizations deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
129 organizations name text Não Não name database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
130 organizations organization_type text Não organization_type IN ('public','private','ngo','education') organization type database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
131 memberships id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
132 memberships source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
133 memberships external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
134 memberships version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
135 memberships data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
136 memberships correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
137 memberships created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
138 memberships updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
139 memberships deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
140 memberships user_id bigint Não Não user id database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
141 memberships organization_id uuid Não Não organization id database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
142 memberships role text Não role IN ('admin','manager','officer','viewer') role database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/role ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/role string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
143 consents id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
144 consents source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
145 consents external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
146 consents version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
147 consents data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
148 consents correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
149 consents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
150 consents updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
151 consents deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
152 consents user_id bigint Sim Não user id database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
153 consents subject_ref text Não Não subject ref database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
154 consents purpose text Não Não purpose database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\zones\openapi.json#/components/schemas/Geometry/properties/purpose string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
155 consents status text Não status IN ('granted','withdrawn','expired') status database/migrations/2026_09_24_001000_investmoz_identity.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
156 consents granted_at timestamptz Sim Não granted at database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
157 consents withdrawn_at timestamptz Sim Não withdrawn at database/migrations/2026_09_24_001000_investmoz_identity.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
158 documents id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_002000_investmoz_content.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
159 documents source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
160 documents external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
161 documents version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_002000_investmoz_content.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
162 documents data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
163 documents correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_002000_investmoz_content.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
164 documents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
165 documents updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_002000_investmoz_content.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
166 documents deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
167 documents file_name text Não Não file name database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
168 documents media_type text Não Não media type database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
169 documents storage_key text Não Não Chave única no armazenamento database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
170 documents sha256 char(64) Não Não SHA-256 hexadecimal; 64 caracteres minúsculos database/migrations/2026_09_24_002000_investmoz_content.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
171 documents status text Não status IN ('active','quarantined','deleted') status database/migrations/2026_09_24_002000_investmoz_content.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
172 geo_locations id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
173 geo_locations source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
174 geo_locations external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
175 geo_locations version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
176 geo_locations data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
177 geo_locations correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
178 geo_locations created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
179 geo_locations updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
180 geo_locations deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
181 geo_locations name text Não Não name database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
182 geo_locations location_type text Não location_type IN ('country','province','district','municipality','site') location type database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
183 geo_locations parent_id uuid Sim Não parent id database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
184 geo_locations code text Não Não code database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
185 geo_geometries id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
186 geo_geometries source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
187 geo_geometries external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
188 geo_geometries version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
189 geo_geometries data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
190 geo_geometries correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
191 geo_geometries created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
192 geo_geometries updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_003000_investmoz_geography.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
193 geo_geometries deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
194 geo_geometries geo_location_id uuid Não Não geo location id database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
195 geo_geometries geometry_type text Não geometry_type IN ('point','line','polygon','multipolygon') Tipo declarado coerente com GeometryType(geom) database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
196 geo_geometries geom geometry(Geometry,4326) Não Não Geometria 2D WGS84 SRID 4326, válida e não vazia database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
197 geo_geometries properties jsonb Não Não Propriedades geográficas; schema aplicacional pendente database/migrations/2026_09_24_003000_investmoz_geography.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
198 projects id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
199 projects source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
200 projects external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
201 projects version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
202 projects data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
203 projects correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
204 projects created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
205 projects updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
206 projects deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
207 projects code text Não Não code database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
208 projects name text Não Não name database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
209 projects status text Não status IN ('draft','planned','active','suspended','completed','cancelled') status database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
210 projects description text Sim Não description database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
211 projects budget_amount numeric(20,2) Sim Não Orçamento exacto não negativo database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
212 projects budget_currency char(3) Sim Não Código monetário do orçamento; lista ISO autorizada pendente database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
213 projects start_date date Sim Não start date database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
214 projects end_date date Sim Não end date database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/end_date ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/end_date string|null date NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
215 projects metadata jsonb Não Não Metadados flexíveis; schema aplicacional pendente database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
216 project_locations id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
217 project_locations source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
218 project_locations external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
219 project_locations version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
220 project_locations data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
221 project_locations correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
222 project_locations created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
223 project_locations updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
224 project_locations deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
225 project_locations project_id uuid Não Não project id database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
226 project_locations geo_location_id uuid Não Não geo location id database/migrations/2026_09_24_004000_investmoz_projects.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
227 project_locations role text Não role IN ('primary','secondary','impact') role database/migrations/2026_09_24_004000_investmoz_projects.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/role ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/role string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
228 economic_clusters id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
229 economic_clusters source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
230 economic_clusters external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
231 economic_clusters version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
232 economic_clusters data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
233 economic_clusters correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
234 economic_clusters created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
235 economic_clusters updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
236 economic_clusters deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
237 economic_clusters code text Não Não code database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
238 economic_clusters name text Não Não name database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
239 economic_clusters sector text Não Não sector database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/sector ; docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/sector object|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
240 economic_clusters status text Não status IN ('planned','active','inactive') status database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
241 economic_clusters metadata jsonb Não Não Metadados flexíveis; schema aplicacional pendente database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
242 value_chains id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
243 value_chains source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
244 value_chains external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
245 value_chains version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
246 value_chains data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
247 value_chains correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
248 value_chains created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
249 value_chains updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
250 value_chains deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
251 value_chains cluster_id uuid Não Não cluster id database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
252 value_chains code text Não Não code database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
253 value_chains name text Não Não name database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
254 value_chains stage text Não stage IN ('input','production','processing','logistics','market') stage database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
255 value_chain_links id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
256 value_chain_links source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
257 value_chain_links external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
258 value_chain_links version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
259 value_chain_links data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
260 value_chain_links correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
261 value_chain_links created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
262 value_chain_links updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
263 value_chain_links deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
264 value_chain_links from_value_chain_id uuid Não Não from value chain id database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
265 value_chain_links to_value_chain_id uuid Não Não to value chain id database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
266 value_chain_links link_type text Não link_type IN ('supplies','processes','distributes','supports') link type database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
267 cluster_projects id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
268 cluster_projects source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
269 cluster_projects external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
270 cluster_projects version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
271 cluster_projects data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
272 cluster_projects correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
273 cluster_projects created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
274 cluster_projects updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
275 cluster_projects deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
276 cluster_projects cluster_id uuid Não Não cluster id database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
277 cluster_projects project_id uuid Não Não project id database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
278 cluster_projects role text Não role IN ('anchor','complementary','supporting') role database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/role ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/role string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
279 anchor_project_links id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
280 anchor_project_links source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
281 anchor_project_links external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
282 anchor_project_links version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
283 anchor_project_links data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
284 anchor_project_links correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
285 anchor_project_links created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
286 anchor_project_links updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
287 anchor_project_links deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
288 anchor_project_links anchor_project_id uuid Não Não anchor project id database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
289 anchor_project_links cluster_id uuid Não Não cluster id database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
290 anchor_project_links rationale text Não Não rationale database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
291 complementary_project_links id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
292 complementary_project_links source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
293 complementary_project_links external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
294 complementary_project_links version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
295 complementary_project_links data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
296 complementary_project_links correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
297 complementary_project_links created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
298 complementary_project_links updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_005000_investmoz_clusters.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
299 complementary_project_links deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
300 complementary_project_links anchor_project_id uuid Não Não anchor project id database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
301 complementary_project_links complementary_project_id uuid Não Não complementary project id database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
302 complementary_project_links relation_type text Não relation_type IN ('dependency','enabler','supplier','market') relation type database/migrations/2026_09_24_005000_investmoz_clusters.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
303 development_corridors id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
304 development_corridors source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
305 development_corridors external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
306 development_corridors version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
307 development_corridors data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
308 development_corridors correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
309 development_corridors created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
310 development_corridors updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
311 development_corridors deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
312 development_corridors code text Não Não code database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
313 development_corridors name text Não Não name database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
314 development_corridors status text Não status IN ('proposed','active','closed') status database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
315 development_corridors description text Sim Não description database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
316 corridor_locations id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
317 corridor_locations source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
318 corridor_locations external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
319 corridor_locations version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
320 corridor_locations data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
321 corridor_locations correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
322 corridor_locations created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
323 corridor_locations updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
324 corridor_locations deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
325 corridor_locations corridor_id uuid Não Não corridor id database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/corridor_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/corridor_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/corridor_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
326 corridor_locations geo_location_id uuid Não Não geo location id database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
327 corridor_locations sequence_no integer Não Não sequence no database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
328 corridor_projects id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
329 corridor_projects source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
330 corridor_projects external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
331 corridor_projects version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
332 corridor_projects data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
333 corridor_projects correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
334 corridor_projects created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
335 corridor_projects updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
336 corridor_projects deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
337 corridor_projects corridor_id uuid Não Não corridor id database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/corridor_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/corridor_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/corridor_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
338 corridor_projects project_id uuid Não Não project id database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
339 corridor_clusters id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
340 corridor_clusters source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
341 corridor_clusters external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
342 corridor_clusters version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
343 corridor_clusters data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
344 corridor_clusters correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
345 corridor_clusters created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
346 corridor_clusters updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
347 corridor_clusters deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_006000_investmoz_corridors.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
348 corridor_clusters corridor_id uuid Não Não corridor id database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/corridor_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/corridor_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/corridor_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
349 corridor_clusters cluster_id uuid Não Não cluster id database/migrations/2026_09_24_006000_investmoz_corridors.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/cluster_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/cluster_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/cluster_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
350 investors id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
351 investors source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
352 investors external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
353 investors version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
354 investors data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
355 investors correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
356 investors created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
357 investors updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
358 investors deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
359 investors organization_id uuid Sim Não organization id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
360 investors legal_name text Não Não legal name database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
361 investors investor_type text Não investor_type IN ('domestic','foreign','institutional','individual') investor type database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
362 investors status text Não status IN ('prospect','active','inactive') status database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
363 contacts id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
364 contacts source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
365 contacts external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
366 contacts version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
367 contacts data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
368 contacts correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
369 contacts created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
370 contacts updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
371 contacts deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
372 contacts full_name text Não Não full name database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/full_name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
373 contacts email text Sim Não email database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/email string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
374 contacts phone text Sim Não phone database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/phone string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
375 investor_contacts investor_id uuid Não Não investor id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
376 investor_contacts contact_id uuid Não Não contact id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
377 investor_contacts is_primary boolean Não Não Contacto principal deste investidor; limite quantitativo pendente database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/is_primary ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/is_primary boolean NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
378 investor_contacts created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
379 investor_contacts updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
380 leads id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
381 leads source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
382 leads external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
383 leads version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
384 leads data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
385 leads correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
386 leads created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
387 leads updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
388 leads deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
389 leads investor_id uuid Sim Não investor id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
390 leads owner_user_id bigint Sim Não owner user id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
391 leads title text Não Não title database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
392 leads status text Não status IN ('new','qualified','disqualified','converted') status database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
393 leads estimated_amount numeric(20,2) Sim Não Montante estimado exacto não negativo database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/estimated_amount number NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
394 leads currency char(3) Sim Não Código monetário; três letras maiúsculas; lista ISO autorizada pendente database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/currency string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
395 crm_opportunities id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
396 crm_opportunities source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
397 crm_opportunities external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
398 crm_opportunities version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
399 crm_opportunities data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
400 crm_opportunities correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
401 crm_opportunities created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
402 crm_opportunities updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
403 crm_opportunities deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
404 crm_opportunities lead_id uuid Sim Não lead id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
405 crm_opportunities project_id uuid Sim Não project id database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
406 crm_opportunities owner_user_id bigint Sim Não owner user id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
407 crm_opportunities stage text Não stage IN ('discovery','assessment','negotiation','approved','lost') stage database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
408 crm_opportunities amount numeric(20,2) Sim Não Montante monetário exacto não negativo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
409 crm_opportunities currency char(3) Sim Não Código monetário; três letras maiúsculas; lista ISO autorizada pendente database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Payload/properties/currency string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
410 crm_opportunities probability numeric(5,2) Sim Não probability database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
411 activities id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
412 activities source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
413 activities external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
414 activities version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
415 activities data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
416 activities correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
417 activities created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
418 activities updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
419 activities deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
420 activities investor_id uuid Sim Não investor id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
421 activities opportunity_id uuid Sim Não opportunity id database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
422 activities actor_user_id bigint Sim Não actor user id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
423 activities activity_type text Não activity_type IN ('call','meeting','email','note') activity type database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
424 activities occurred_at timestamptz Não Não occurred at database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
425 activities summary text Não Não summary database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/summary string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
426 tasks id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
427 tasks source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
428 tasks external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
429 tasks version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
430 tasks data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
431 tasks correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
432 tasks created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
433 tasks updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
434 tasks deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
435 tasks assignee_user_id bigint Sim Não assignee user id database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
436 tasks opportunity_id uuid Sim Não opportunity id database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
437 tasks title text Não Não title database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
438 tasks status text Não status IN ('open','in_progress','done','cancelled') status database/migrations/2026_09_24_007000_investmoz_crm.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
439 tasks due_at timestamptz Sim Não due at database/migrations/2026_09_24_007000_investmoz_crm.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
440 investor_journeys id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
441 investor_journeys source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
442 investor_journeys external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
443 investor_journeys version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
444 investor_journeys data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
445 investor_journeys correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
446 investor_journeys created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
447 investor_journeys updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
448 investor_journeys deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
449 investor_journeys investor_id uuid Não Não investor id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
450 investor_journeys opportunity_id uuid Sim Não opportunity id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/opportunity_id integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
451 investor_journeys status text Não status IN ('started','active','completed','cancelled') status database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
452 investor_journeys started_at timestamptz Não Não started at database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
453 journey_steps id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
454 journey_steps source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
455 journey_steps external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
456 journey_steps version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
457 journey_steps data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
458 journey_steps correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
459 journey_steps created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
460 journey_steps updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
461 journey_steps deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
462 journey_steps journey_id uuid Não Não journey id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
463 journey_steps step_code text Não Não step code database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
464 journey_steps sequence_no integer Não Não sequence no database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
465 journey_steps status text Não status IN ('pending','active','completed','blocked') status database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
466 submissions id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
467 submissions source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
468 submissions external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
469 submissions version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
470 submissions data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
471 submissions correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
472 submissions created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
473 submissions updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
474 submissions deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
475 submissions journey_step_id uuid Não Não journey step id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
476 submissions submitted_by bigint Sim Não submitted by database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
477 submissions status text Não status IN ('draft','submitted','accepted','rejected') status database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
478 submissions payload jsonb Não Não Conteúdo da submissão/evento; contrato por tipo pendente database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload |object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
479 submissions submitted_at timestamptz Sim Não submitted at database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
480 aftercare_plans id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
481 aftercare_plans source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
482 aftercare_plans external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
483 aftercare_plans version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
484 aftercare_plans data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
485 aftercare_plans correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
486 aftercare_plans created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
487 aftercare_plans updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
488 aftercare_plans deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
489 aftercare_plans investor_id uuid Não Não investor id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
490 aftercare_plans project_id uuid Sim Não project id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
491 aftercare_plans owner_user_id bigint Sim Não owner user id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
492 aftercare_plans status text Não status IN ('draft','active','closed') status database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
493 aftercare_plans review_date date Sim Não review date database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
494 constraints id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
495 constraints source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
496 constraints external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
497 constraints version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
498 constraints data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
499 constraints correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
500 constraints created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
501 constraints updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
502 constraints deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
503 constraints aftercare_plan_id uuid Não Não aftercare plan id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
504 constraints category text Não Não category database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/category string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
505 constraints description text Não Não description database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/description string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
506 constraints severity text Não severity IN ('low','medium','high','critical') severity database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
507 constraints status text Não status IN ('open','mitigating','resolved') status database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
508 escalations id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
509 escalations source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
510 escalations external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
511 escalations version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
512 escalations data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
513 escalations correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
514 escalations created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
515 escalations updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
516 escalations deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
517 escalations constraint_id uuid Não Não constraint id database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
518 escalations escalated_by bigint Sim Não escalated by database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
519 escalations level text Não level IN ('operational','management','executive') level database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
520 escalations status text Não status IN ('open','acknowledged','closed') status database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
521 escalations escalated_at timestamptz Não Não escalated at database/migrations/2026_09_24_008000_investmoz_journey_aftercare.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
522 skills id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
523 skills source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
524 skills external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
525 skills version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
526 skills data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
527 skills correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
528 skills created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
529 skills updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
530 skills deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
531 skills code text Não Não code database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
532 skills name text Não Não name database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
533 occupations id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
534 occupations source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
535 occupations external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
536 occupations version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
537 occupations data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
538 occupations correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
539 occupations created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
540 occupations updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
541 occupations deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
542 occupations code text Não Não code database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
543 occupations name text Não Não name database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
544 training_needs id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
545 training_needs source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
546 training_needs external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
547 training_needs version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
548 training_needs data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
549 training_needs correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
550 training_needs created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
551 training_needs updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
552 training_needs deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
553 training_needs project_id uuid Sim Não project id database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
554 training_needs occupation_id uuid Sim Não occupation id database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/occupation_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/occupation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
555 training_needs skill_id uuid Sim Não skill id database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
556 training_needs quantity integer Não Não quantity database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/quantity ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/quantity ; docs\investmoz\unification\openapi.json#/components/schemas/Need/properties/quantity integer|number NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
557 training_needs priority text Não priority IN ('Must','Should','Could') priority database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\zones\openapi.json#/components/schemas/Link/properties/priority ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/priority integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
558 training_providers id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
559 training_providers source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
560 training_providers external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
561 training_providers version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
562 training_providers data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
563 training_providers correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
564 training_providers created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
565 training_providers updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
566 training_providers deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
567 training_providers organization_id uuid Sim Não organization id database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
568 training_providers name text Não Não name database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/name ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/name string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
569 training_providers status text Não status IN ('candidate','approved','inactive') status database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
570 courses id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
571 courses source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
572 courses external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
573 courses version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
574 courses data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
575 courses correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
576 courses created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
577 courses updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
578 courses deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
579 courses provider_id uuid Não Não provider id database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
580 courses code text Não Não code database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/code ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/code ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/code string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
581 courses title text Não Não title database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/title ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/title ; docs\investmoz\zones\openapi.json#/components/schemas/LegalSource/properties/title string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
582 courses status text Não status IN ('draft','published','retired') status database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
583 education_opportunities id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
584 education_opportunities source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
585 education_opportunities external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
586 education_opportunities version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
587 education_opportunities data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
588 education_opportunities correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
589 education_opportunities created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
590 education_opportunities updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
591 education_opportunities deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
592 education_opportunities course_id uuid Não Não course id database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
593 education_opportunities geo_location_id uuid Sim Não geo location id database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
594 education_opportunities start_date date Não Não start date database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
595 education_opportunities end_date date Sim Não end date database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/end_date ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/end_date string|null date NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
596 education_opportunities capacity integer Sim Não capacity database/migrations/2026_09_24_009000_investmoz_education.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
597 education_opportunities status text Não status IN ('planned','open','closed','cancelled') status database/migrations/2026_09_24_009000_investmoz_education.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
598 project_documents project_id uuid Não Não project id database/migrations/2026_09_24_009500_investmoz_document_links.php docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandInput/properties/project_id ; docs\investmoz\education\openapi.proposed.json#/components/schemas/DemandPatch/properties/project_id ; docs\investmoz\zones\openapi.json#/components/schemas/Relation/properties/project_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
599 project_documents document_id uuid Não Não document id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
600 project_documents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
601 investor_documents investor_id uuid Não Não investor id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
602 investor_documents document_id uuid Não Não document id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
603 investor_documents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
604 journey_documents journey_id uuid Não Não journey id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
605 journey_documents document_id uuid Não Não document id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
606 journey_documents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
607 aftercare_documents aftercare_plan_id uuid Não Não aftercare plan id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
608 aftercare_documents document_id uuid Não Não document id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
609 aftercare_documents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
610 education_documents education_opportunity_id uuid Não Não education opportunity id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
611 education_documents document_id uuid Não Não document id database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
612 education_documents created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_009500_investmoz_document_links.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
613 audit_events id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_010000_investmoz_governance_ai.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
614 audit_events source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
615 audit_events external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
616 audit_events data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
617 audit_events correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_010000_investmoz_governance_ai.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
618 audit_events created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
619 audit_events actor_user_id bigint Sim Não actor user id database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
620 audit_events entity_type text Não Não entity type database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
621 audit_events entity_id uuid Sim Não ID histórico UUID sem FK; a entidade pode já não existir database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
622 audit_events action text Não Não action database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
623 audit_events occurred_at timestamptz Não Não occurred at database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
624 audit_events before_data jsonb Sim Não Representação anterior; aplicação deve remover segredos database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
625 audit_events after_data jsonb Sim Não Representação posterior; aplicação deve remover segredos database/migrations/2026_09_24_010000_investmoz_governance_ai.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
626 outbox_events id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
627 outbox_events source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
628 outbox_events external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
629 outbox_events version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
630 outbox_events data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
631 outbox_events correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
632 outbox_events created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
633 outbox_events updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
634 outbox_events aggregate_type text Não Não aggregate type database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
635 outbox_events aggregate_id uuid Não Não UUID do agregado produtor; sem FK polimórfica database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
636 outbox_events event_type text Não Não event type database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
637 outbox_events payload jsonb Não Não Conteúdo da submissão/evento; contrato por tipo pendente database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload |object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
638 outbox_events status text Não status IN ('pending','published','failed') status database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
639 outbox_events available_at timestamptz Não Não available at database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
640 outbox_events published_at timestamptz Sim Não published at database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
641 inbox_events id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
642 inbox_events source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
643 inbox_events external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
644 inbox_events version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
645 inbox_events data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
646 inbox_events correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
647 inbox_events created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
648 inbox_events updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
649 inbox_events message_id text Não Não message id database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
650 inbox_events event_type text Não Não event type database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
651 inbox_events payload jsonb Não Não Conteúdo da submissão/evento; contrato por tipo pendente database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/CompanyDeclaration/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/Draft/properties/payload ; docs\investmoz\unification\openapi.json#/components/schemas/InvestorAccount/properties/payload |object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
652 inbox_events status text Não status IN ('received','processed','failed') status database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
653 inbox_events received_at timestamptz Não Não received at database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
654 inbox_events processed_at timestamptz Sim Não processed at database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
655 sync_runs id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
656 sync_runs source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
657 sync_runs external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
658 sync_runs version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
659 sync_runs data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
660 sync_runs correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
661 sync_runs created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
662 sync_runs updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
663 sync_runs deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
664 sync_runs integration_name text Não Não integration name database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
665 sync_runs direction text Não direction IN ('inbound','outbound') direction database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
666 sync_runs status text Não status IN ('running','succeeded','failed','cancelled') status database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
667 sync_runs started_at timestamptz Não Não started at database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
668 sync_runs ended_at timestamptz Sim Não ended at database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
669 sync_runs totals jsonb Não Não totals database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
670 reconciliation_issues id uuid Não Não Identificador primário da entidade database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/id ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Entity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/id ; docs\investmoz\unification\openapi.json#/components/schemas/Option/properties/id integer|string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
671 reconciliation_issues source_system text Não Não Sistema de origem; integra a chave estável de importação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
672 reconciliation_issues external_id text Sim Não Identificador na origem; NULL quando não fornecido database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
673 reconciliation_issues version_no integer Não Não Versão positiva; comparação compete ao importador database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Version/properties/version_no ; docs\investmoz\zones\openapi.json#/components/schemas/Zone/properties/version_no integer NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
674 reconciliation_issues data_classification text Não data_classification IN ('public','internal','confidential','restricted') Classificação de acesso; não substitui autorização database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
675 reconciliation_issues correlation_id uuid Sim Não Correlação entre operações; não é FK database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/correlation_id string uuid NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
676 reconciliation_issues created_at timestamptz Não Não Instante de criação database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
677 reconciliation_issues updated_at timestamptz Não Não Instante da última actualização database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Demand/properties/updated_at string date-time NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
678 reconciliation_issues deleted_at timestamptz Sim Não Eliminação lógica; NULL indica activo database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
679 reconciliation_issues sync_run_id uuid Não Não sync run id database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
680 reconciliation_issues entity_type text Não Não entity type database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
681 reconciliation_issues external_id_ref text Sim Não external id ref database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
682 reconciliation_issues issue_type text Não issue_type IN ('missing','duplicate','orphan','checksum','geometry','value') issue type database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
683 reconciliation_issues severity text Não severity IN ('warning','error') severity database/migrations/2026_09_24_011000_investmoz_integrations.php NAO_REGISTADA_NO_DICIONARIO SEM_CAMPO_OPENAPI_DOCUMENTADO
684 reconciliation_issues status text Não status IN ('open','accepted','resolved') status database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\education\openapi.proposed.json#/components/schemas/Problem/properties/status ; docs\investmoz\existing-business\openapi.json#/components/schemas/Business/properties/status ; docs\investmoz\unification\openapi.json#/components/schemas/Opportunity/properties/status |integer|string NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
685 reconciliation_issues details jsonb Não Não details database/migrations/2026_09_24_011000_investmoz_integrations.php docs\investmoz\unification\openapi.json#/components/schemas/Need/properties/details object NAO_REGISTADA_NO_DICIONARIO NOME_CORRESPONDENTE_POR_CONFIRMAR_SEMANTICA
@@ -0,0 +1,52 @@
# Progresso do mercado de negócios — 30/09/2026
## Implementado nesta execução
- Extensão do modelo existente `existing_businesses` e `interest_requests`; não foi criada uma segunda entidade de negócio.
- Fluxo de triagem, revisão, aprovação de nível 1, aprovação de nível 2 e publicação, com políticas por email designado, conta verificada, permissão de gestão e MFA. A aprovação de nível 1, nível 2 e publicação exigem actores distintos.
- Catálogo público servido por projecção de campos permitidos. A ficha pública não serializa payload privado, proponente, documentos nem geometria exacta.
- Registo público de manifestação de interesse com idempotência, consentimento registado e deduplicação de pedidos activos para o mesmo negócio, email e modalidade.
- Ciclo de estados de interesse validado pelo serviço `InterestWorkflow`. Cada mudança efectiva guarda actor, estados anterior/seguinte, nota cifrada e correlação no histórico; a ficha de gestão apresenta histórico e próxima acção/prazo.
- Configuração de designações documentada em `.env.example`. Listas vazias não concedem acesso.
## Limites ainda activos
- O fluxo de transição do interesse e a ficha de gestão estão implementados, mas a atribuição a responsáveis ainda não tem interface de gestão.
- O prazo é guardado, mas não há job de SLA, alertas de atraso ou escalonamento automático.
- A outbox recebe eventos de submissão. O worker CRM para este novo fluxo, reconciliação e dead-letter ainda precisam de implementação e homologação; nenhum evento foi enviado para um serviço externo nesta execução.
- Os anexos ainda não têm upload privado com antivírus, retenção automatizada ou sala segura. Não tratar referências documentais declaradas como ficheiros validados.
- Consentimento de contacto obrigatório é registado no pedido. Consentimentos separados para partilha com o proponente/terceiros e respectivos fluxos de revogação continuam pendentes.
- A disponibilização das rotas públicas não constitui autorização institucional para publicar oportunidades reais. Confirmar designações e conteúdo público antes de activar em produção.
## Evidência reproduzida
- `php artisan test --filter='PortalManagementTest|InvestmentWorkspaceTest|BusinessMarketplacePublicTest|ExistingBusinessIntakeTest'`: 20 testes, 301 verificações aprovadas, usando exclusivamente `portal_services_test` em `127.0.0.1:55441`.
- `npm.cmd run types:check`: TypeScript sem erros.
- Laravel Pint `--test` nos PHP alterados: aprovado.
- PHP 8.5.11 explícito: lint sem erros nos novos serviços/controladores/teste.
Todos os comandos PHP desta execução usaram `C:\Users\macbook\.config\investmoz\php-8.5.11\php.exe`.
## Continuação — tarefas operacionais
Esta actualização substitui a limitação anterior sobre atribuição: a ficha de gestão permite agora seleccionar um responsável entre gestores verificados incluídos nas listas de designação configuradas. A fila apresenta o responsável e o prazo.
Quando a próxima acção e o prazo são definidos, é criada uma tarefa privada em `tasks`, ligada à manifestação, com responsável, classificação confidencial e data limite no fim do dia local. A chave externa determinística impede duplicados ao guardar novamente a mesma acção. Uma acção alterada ou removida encerra a tarefa aberta anterior como `cancelled`/`superseded`, preservando o registo.
Validação adicional: mesmos 20 testes focados, agora com 323 verificações; TypeScript sem erros. Os prazos são acompanhados na fila e nas tarefas; não há ainda notificações automáticas nem escalonamento de SLA.
A fila de gestão mostra a acção, o responsável e o prazo; tarefas vencidas são destacadas visualmente. Actualizar apenas o estado preserva a atribuição e a próxima acção existentes. Guardar a mesma acção novamente não duplica a tarefa.
A fila também oferece filtros para prazos vencidos, prazos futuros e pedidos sem responsável. O filtro de prazo exclui estados terminais.
## Actualização — consentimentos separados
O formulário regista separadamente o consentimento obrigatório para responder ao pedido (`respond_to_interest`) e a autorização opcional para partilhar contacto com o proponente (`share_contact_with_proponent`). Cada registo conserva versão da política, data, referência do pedido e classificação confidencial. A opção de partilha começa desmarcada e não condiciona a submissão.
A gestão pode revogar a autorização de partilha com motivo obrigatório. A revogação actualiza o registo de consentimento, guarda o motivo cifrado no histórico e cria evento de outbox sem email, telefone ou texto da mensagem. Este procedimento administrativo implementa revogação registada; ainda falta um fluxo de auto-serviço para o titular.
Validação desta continuação: 29 testes focados / 381 verificações aprovadas; TypeScript e build aprovados; Pint aprovado nos ficheiros PHP alterados. O build continua a emitir apenas o aviso de dependência opcional `fontaine`.
## Escalada de tarefas vencidas
O comando `investmoz:escalate-overdue-tasks` eleva para prioridade 1 as tarefas `triage` e `interest_follow_up` vencidas que ainda estejam abertas/em curso, marcando `outcome=sla_overdue`. O scheduler agenda a verificação a cada 15 minutos. A função vem desactivada; habilitar `INVESTMOZ_SLA_ESCALATION_ENABLED=true` apenas quando o scheduler Laravel estiver operacional. A escalada não envia email nem integrações externas.
@@ -0,0 +1,48 @@
# Integração do modelo de negócios
O SQL recebido foi convertido numa migração incremental para PostgreSQL/PostGIS. `existing_businesses` continua a ser a entidade canónica dos anúncios de negócios; não é criado outro catálogo de oportunidades. `interest_requests` continua a guardar manifestações de interesse já recebidas.
## Correspondências
| SQL recebido | Modelo actual |
| --- | --- |
| `business.parties` | Contas autenticadas em `users`; proponentes institucionais em `organizations`; perfis de investimento em `investors` e `investor_accounts`. Não é criada uma segunda tabela de identidade. |
| `business.opportunities` | `existing_businesses`: `tracking_code` usa `code`; proponente/criador usa `owner_user_id`; proprietário institucional usa `organization_id`; sector usa `sector_id`; versão usa `version_no`. |
| `business.opportunity_locations` | Nova tabela de relações `existing_business_locations`, ligada a `geo_locations` por UUID e com validação da hierarquia e geometria PostGIS. |
| `business.assets` | Campos tipados adicionais em `existing_business_objects`; o `payload` cifrado actual continua a guardar pormenores privados. |
| `business.rights` | Nova tabela `existing_business_rights`, ligada ao negócio e, quando aplicável, ao objecto. O detalhe reservado fica em `confidential_payload`, que os serviços devem cifrar. |
| `business.documents` | `documents` actual (`file_name`, `media_type`, `storage_key`, `sha256`, `status` e `data_classification`), com metadados adicionais; nova tabela de ligação ao negócio ou ao pedido de interesse. |
| `business.investor_profiles` | Campos de perfil em `investors`, cuja ligação à conta autenticada já existe em `investor_accounts`. |
| `business.interests` | `interest_requests`; acrescenta alvo `existing_business_id`, perfil do investidor, modalidade, montantes e atribuição. `opportunity_id` continua a referir-se ao catálogo público de investimentos, e uma submissão só pode indicar um desses dois alvos. |
| `business.consents` | `consents` actual, com alvo opcional e versão da política; os estados existentes `granted`/`withdrawn` mantêm-se como fonte canónica. |
| `business.workflow_transitions` | Histórico actual de revisões/reviews de negócios mais `business_workflow_transitions` para transições de negócios e pedidos de interesse. |
| `business.tasks` / `business.outbox_messages` | `tasks` e `outbox_events` existentes recebem campos adicionais; não são criadas filas paralelas. |
| `business.crm_links` | Nova tabela `external_record_links` para mapear identificadores remotos sem duplicar os registos de CRM. |
Campos confidenciais já protegidos no `payload` cifrado não são copiados para colunas em texto aberto. O campo `public_title` fica separado para publicação futura; esta migração não publica negócios nem muda a máquina de estados da aplicação.
## Migração e reversão
Aplicar em produção depois do backup normal da base:
```sh
php artisan migrate --force
```
A reversão remove apenas colunas e tabelas novas, e recusa prosseguir quando já houver dados nelas. Não executa `DROP SCHEMA ... CASCADE` e não remove dados existentes.
Esta entrega altera o esquema e a serialização do modelo. Os endpoints/formulários para gerir todas as novas relações, estados de aprovação, documentos e tarefas requerem implementação incremental própria; o SQL por si só não os activa.
## Arquitetura da aplicação (opção 1)
A implementação da proposta reutiliza os serviços e modelos que já existem, sem introduzir `Opportunity`, `Interest`, `Party` ou `OutboxMessage` paralelos:
| Responsabilidade proposta | Implementação no portal |
| --- | --- |
| Criar e actualizar rascunhos | `ExistingBusinessDrafts`, usado pela API autenticada `api/v1/existing-business/listings`. |
| Submeter e rever anúncios | `ExistingBusinessIntake`, com autorização via `ExistingBusinessPolicy`, controlo de versão e histórico cifrado. |
| Pedidos de interesse idempotentes | `InterestRequestController` e `InterestSubmission`; eventos são gravados na outbox existente. |
| Integração CRM | Cliente seguro `SuiteCrmClient`; a outbox existente usa `aggregate_type`, `aggregate_id`, `event_type`, `payload` e estados `pending/published/failed`. |
| Estados tipados | Enums `OpportunityStatus` e `InterestStatus` em `app/Domain`, mantendo os valores de estado já persistidos. |
Os estados adicionais do enum `OpportunityStatus` alimentam agora as etapas de triagem, revisão, duas aprovações e publicação de `ExistingBusinessIntake`. A publicação usa uma projecção pública separada e allowlist de campos. As manifestações de interesse têm uma máquina de estados própria em `InterestWorkflow`; transições, notas cifradas e prazos são guardados no histórico comum. Isto ainda não constitui uma integração CRM em background, SLA automatizado, upload/scanner de documentos ou autorização institucional para publicar dados reais.
+128
View File
@@ -0,0 +1,128 @@
{
"openapi": "3.1.0",
"info": {
"title": "InvestMoz reference catalogues",
"version": "1.0.0",
"description": "Read-only, rate-limited reference options with stable codes, version metadata and pt-MZ labels. Jurisdictions use the packaged territorial reference set. Economic activities are portal-curated sector groupings, not an official statutory classification. Authority entries are withheld until an authoritative source is approved."
},
"servers": [{ "url": "/api/v1/catalogs" }],
"security": [],
"paths": {
"/authorities": { "$ref": "#/components/pathItems/Authorities" },
"/jurisdictions": { "$ref": "#/components/pathItems/Jurisdictions" },
"/economic-activities": { "$ref": "#/components/pathItems/EconomicActivities" }
},
"components": {
"pathItems": {
"Authorities": {
"get": {
"summary": "Search the published authorities catalogue",
"parameters": [
{ "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/version" },
{ "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/per_page" },
{ "name": "jurisdiction_id", "in": "query", "schema": { "type": "string", "format": "uuid" } }
],
"responses": { "200": { "$ref": "#/components/responses/CatalogPage" }, "429": { "description": "Rate limit exceeded" } }
}
},
"Jurisdictions": {
"get": {
"summary": "List territorial reference options for dependent dropdowns",
"parameters": [
{ "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/version" },
{ "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/per_page" },
{ "name": "type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["country", "province", "district", "admin_post", "locality"] } },
{ "name": "parent_id", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Required for district, admin_post and locality; the parent must be of the immediately preceding level." }
],
"responses": { "200": { "$ref": "#/components/responses/CatalogPage" }, "422": { "description": "Invalid or missing parent selection" }, "429": { "description": "Rate limit exceeded" } }
}
},
"EconomicActivities": {
"get": {
"summary": "Search the portal-curated economic activity groupings",
"parameters": [
{ "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/version" },
{ "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/per_page" },
{ "name": "parent_id", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Optional parent activity for hierarchical catalog browsing." },
{ "name": "sector_id", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "Optional sector filter for the dependent activity dropdown." }
],
"responses": { "200": { "$ref": "#/components/responses/CatalogPage" }, "429": { "description": "Rate limit exceeded" } }
}
}
},
"parameters": {
"q": { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 100 } },
"version": { "name": "version", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "A published immutable version; omitted selects the current effective version." },
"page": { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
"per_page": { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }
},
"responses": {
"CatalogPage": {
"description": "Stable option values with Portuguese (Mozambique) labels. A not-yet-published catalogue returns an empty page and status not_published.",
"headers": {
"Cache-Control": { "schema": { "type": "string" } },
"ETag": { "schema": { "type": "string" } }
},
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["data", "links", "meta", "next_page_url", "prev_page_url", "current_page", "per_page", "total", "last_page"],
"properties": {
"data": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogEntry" } },
"links": { "type": "object", "properties": { "next": { "type": ["string", "null"] }, "previous": { "type": ["string", "null"] } } },
"next_page_url": { "type": ["string", "null"] },
"prev_page_url": { "type": ["string", "null"] },
"current_page": { "type": "integer" },
"per_page": { "type": "integer" },
"total": { "type": "integer" },
"last_page": { "type": "integer" },
"meta": { "$ref": "#/components/schemas/CatalogMetadata" }
}
}
}
}
}
},
"schemas": {
"CatalogEntry": {
"type": "object",
"required": ["id", "value", "code", "name", "label", "locale"],
"properties": {
"id": { "type": "string", "format": "uuid" },
"value": { "type": "string", "description": "Stable API value; never the translated label." },
"code": { "type": "string" },
"name": { "type": "string" },
"label": { "type": "string", "description": "Presentation label in pt-MZ." },
"locale": { "const": "pt-MZ" },
"parent_id": { "type": ["string", "null"], "format": "uuid" },
"type": { "type": "string" },
"version_no": { "type": "integer", "minimum": 1 },
"sector_id": { "type": "integer" },
"source_code": { "type": ["string", "null"] },
"authority_type": { "type": "string" },
"organization_id": { "type": "string", "format": "uuid" },
"jurisdiction_id": { "type": ["string", "null"], "format": "uuid" },
"jurisdiction_name": { "type": ["string", "null"] }
}
},
"CatalogMetadata": {
"type": "object",
"required": ["catalog", "version", "locale", "current_page", "per_page", "total", "last_page"],
"properties": {
"catalog": { "type": "string", "enum": ["authorities", "jurisdictions", "economic-activities"] },
"version": { "type": ["integer", "null"] },
"locale": { "const": "pt-MZ" },
"status": { "type": "string", "enum": ["not_published"] },
"source_version": { "type": ["string", "null"] },
"valid_from": { "type": ["string", "null"], "format": "date" },
"valid_to": { "type": ["string", "null"], "format": "date" },
"current_page": { "type": "integer" },
"per_page": { "type": "integer" },
"total": { "type": "integer" },
"last_page": { "type": "integer" }
}
}
}
}
}
+56
View File
@@ -0,0 +1,56 @@
{
"openapi": "3.1.0",
"info": {"title": "InvestMoz Simulation Execution API", "version": "1.0.0", "description": "Indicative arithmetic scenarios. Rates and reductions are user supplied, not legal rates or eligibility decisions."},
"servers": [{"url": "/api/v1"}],
"paths": {
"/simulation-drafts": {
"post": {"summary": "Create an encrypted, versioned simulator form draft", "parameters": [{"$ref": "#/components/parameters/IdempotencyKey"}, {"$ref": "#/components/parameters/SimulationToken"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SimulationFormDraft"}}}}, "responses": {"201": {"description": "Created with ETag"}, "200": {"description": "Idempotent replay"}, "409": {"description": "Draft exists or key reused with a different payload"}, "422": {"description": "Invalid draft field"}}}
},
"/simulation-drafts/current": {
"get": {"summary": "Read the current user's or anonymous token's draft", "parameters": [{"$ref": "#/components/parameters/SimulationToken"}], "responses": {"200": {"description": "Draft snapshot and ETag; personal fields are decrypted only for its owner"}}}
},
"/simulation-drafts/{id}": {
"get": {"summary": "Read an owned draft", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/SimulationToken"}], "responses": {"200": {"description": "Draft snapshot and ETag"}, "404": {"description": "Draft inaccessible"}}},
"put": {"summary": "Autosave an owned draft with optimistic locking", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/IfMatch"}, {"$ref": "#/components/parameters/SimulationToken"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SimulationFormDraft"}}}}, "responses": {"200": {"description": "Saved with new ETag and snapshot hash"}, "412": {"description": "Stale draft version"}, "422": {"description": "Invalid draft field"}}},
"delete": {"summary": "Soft-delete an owned draft", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/IfMatch"}, {"$ref": "#/components/parameters/SimulationToken"}], "responses": {"204": {"description": "Deleted"}, "412": {"description": "Stale draft version"}}}
},
"/questionnaires/{code}": {"get": {"summary": "Read the published Portuguese questionnaire version", "parameters": [{"name": "code", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "Published questionnaire and stable version", "headers": {"ETag": {"schema": {"type": "string"}}}}, "404": {"description": "No published current version"}}}},
"/simulations": {
"post": {
"summary": "Create a traceable simulation",
"parameters": [{"$ref": "#/components/parameters/IdempotencyKey"}, {"$ref": "#/components/parameters/SimulationToken"}],
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SimulationInput"}}}},
"responses": {"202": {"description": "Execution queued", "headers": {"ETag": {"schema": {"type": "string"}}, "Location": {"schema": {"type": "string"}}}}, "200": {"description": "Idempotent replay"}, "409": {"description": "Key reused with a different payload"}, "422": {"description": "Invalid input or missing anonymous token"}, "428": {"description": "Idempotency-Key required"}}
}
},
"/simulations/{id}": {
"get": {"summary": "Read a simulation and its snapshots", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/SimulationToken"}], "responses": {"200": {"description": "Simulation", "headers": {"ETag": {"schema": {"type": "string"}}}}, "404": {"description": "Missing or outside authorized scope"}}}
},
"/simulations/{id}/scenarios/{scenarioId}": {
"patch": {
"summary": "Update a scenario with optimistic concurrency",
"parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/ScenarioId"}, {"$ref": "#/components/parameters/IfMatch"}, {"$ref": "#/components/parameters/SimulationToken"}],
"requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateScenarioInput"}}}},
"responses": {"202": {"description": "Updated; re-execution queued"}, "403": {"description": "Edit not authorized"}, "404": {"description": "Simulation inaccessible"}, "412": {"description": "Stale ETag"}, "428": {"description": "If-Match required"}, "422": {"description": "Invalid input"}}
}
},
"/simulations/{id}/reports/{reportId}": {
"get": {"summary": "Read an immutable report snapshot", "parameters": [{"$ref": "#/components/parameters/Id"}, {"$ref": "#/components/parameters/ReportId"}, {"$ref": "#/components/parameters/SimulationToken"}], "responses": {"200": {"description": "Report snapshot and hash"}, "404": {"description": "Report inaccessible"}}}
}
},
"components": {
"parameters": {
"Id": {"name": "id", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}},
"ScenarioId": {"name": "scenarioId", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}},
"ReportId": {"name": "reportId", "in": "path", "required": true, "schema": {"type": "string", "format": "uuid"}},
"IdempotencyKey": {"name": "Idempotency-Key", "in": "header", "required": true, "schema": {"type": "string", "minLength": 16, "maxLength": 128}},
"SimulationToken": {"name": "X-Simulation-Token", "in": "header", "required": false, "description": "Required for anonymous access. Client generated; only an HMAC hash is stored.", "schema": {"type": "string", "minLength": 32, "maxLength": 256}},
"IfMatch": {"name": "If-Match", "in": "header", "required": true, "description": "Strong ETag; absent returns 428, stale returns 412.", "schema": {"type": "string"}}
},
"schemas": {
"SimulationInput": {"type": "object", "additionalProperties": false, "required": ["base", "rate", "reduction"], "properties": {"base": {"type": "number", "minimum": 0, "description": "User input; no currency is inferred."}, "rate": {"type": "number", "minimum": 0, "maximum": 100, "description": "User scenario rate; not an official rate."}, "reduction": {"type": "number", "minimum": 0, "maximum": 100, "description": "User supplied scenario reduction; no default is applied."}, "organization_id": {"type": "string", "format": "uuid", "description": "Optional; requires admin, manager or officer membership."}}},
"SimulationFormDraft": {"type": "object", "additionalProperties": false, "required": ["personal_snapshot", "economic_snapshot"], "properties": {"personal_snapshot": {"type": "object", "description": "Allowlisted personal fields, encrypted at rest; only owner/token can read."}, "economic_snapshot": {"type": "object", "required": ["purpose"], "description": "Allowlisted economic/form fields, encrypted at rest; rates remain user-supplied."}}},
"UpdateScenarioInput": {"type": "object", "additionalProperties": false, "required": ["label", "base", "rate", "reduction"], "properties": {"label": {"type": "string", "minLength": 1, "maxLength": 100}, "base": {"type": "number", "minimum": 0}, "rate": {"type": "number", "minimum": 0, "maximum": 100}, "reduction": {"type": "number", "minimum": 0, "maximum": 100, "description": "User supplied scenario reduction; no default is applied."}}}
}
}
}
+119
View File
@@ -0,0 +1,119 @@
# Etapa 10 — Motor fiscal principal
**Estado proposto: BLOQUEADO.** As fórmulas e a matriz de casos foram fornecidas e o utilizador declarou-as formalmente aprovadas em 2026-10-01. Nenhum motor fiscal foi implementado; continuam por satisfazer pré-condições técnicas e por fornecer parâmetros legais de produção.
## Inspecção e conformidade
| Critério | Evidência do repositório | Estado |
| --- | --- | --- |
| Laravel / frontend / dados | Laravel 13.30.1, PHP CLI 8.5.11, React 19/Inertia, PostgreSQL/PostGIS nas migrations e no CI | CONFORME como stack base |
| Regras funcionais | Fórmulas e matriz de casos foram fornecidas e consideradas formalmente aprovadas pelo utilizador em 2026-10-01. `ADR-004` continua sem parâmetros concretos de produção | PARCIAL — falta catálogo de parâmetros/fontes vigentes |
| Snapshots | `SimulationExecution` guarda snapshots de entrada/cálculo/resultado; o questionário é `user-entered-arithmetic`, sem taxa legal (`legal_rate_source: null`) | PARCIAL — infraestrutura genérica, sem snapshot versionado de regras fiscais |
| API estabilizada para os cálculos do MVP | API aceita `base`, `rate` e `reduction` para aritmética genérica; não cobre IRPC, IVA, ISPC, IRPS, INSS ou retenções como regras versionadas. A última tentativa PHPUnit não passou das asserções | BLOQUEADO |
| DSL whitelist e DAG sem ciclos | `ADR-004` descreve uma futura AST tipada como restrição; não há avaliador, validador de DAG ou testes de ciclos no código inspeccionado | AUSENTE |
| Decimal e arredondamento configurado | `ADR-006` exige precisão decimal, mas deixa escala, modo e momento de arredondamento dependentes de aprovação do domínio | BLOQUEADO |
| Casos dourados e fontes | Matriz de casos esperados fornecida e aprovada funcionalmente pelo utilizador; não há fontes legais oficiais associadas nem assinaturas nominais de Fiscalista/QA | PARCIAL — fontes e evidência de revisão pendentes |
| RLS e operação segura | `ADR-007` mantém a matriz IAM/RLS pendente; a eficácia de policies/grants não foi demonstrada nesta execução | BLOQUEADO |
## Divergência de contrato a resolver
A migration existente `2026_10_01_000200_create_simulation_execution_core.php` publica no snapshot de questionário `reduction.default = 0`, enquanto os requests e OpenAPI locais passaram a exigir redução explícita. A migration não foi alterada. É necessário definir uma migração aditiva/versionada do questionário depois de aprovada a semântica; não usar essa divergência como default fiscal.
## Plano condicionado para retomar a Etapa 10
1. Receber catálogo aprovado de parâmetros concretos de produção por imposto, com `scope_key`, período de vigência, fonte oficial e hash; definir precisão e arredondamento.
2. Preservar a matriz funcional já aprovada; completar associação de fundamentos legais e obter as assinaturas nominais de revisão exigidas para release; identificar explicitamente cálculos automáticos e respostas indeterminadas/orientativas.
3. Confirmar API e snapshots do contrato fiscal, incluindo versionamento de regra/engine e exposição de explicabilidade.
4. Confirmar uma base de testes descartável isolada e concluir os gates de IAM/RLS requeridos antes dos testes de integração.
5. Só então propor ficheiros exactos para o avaliador allowlist, validação/topologia DAG, tipos decimais, integração, persistência de fontes e testes dourados. Não se fixam nomes de classes/tabelas sem o pacote aprovado.
## Evidência desta execução
- PHP 8.5.11 foi confirmado; Laravel, OpenAPI, API, migrations, ADRs e testes foram inspeccionados em leitura.
- A API OpenAPI de simulações declara o cenário aritmético genérico; a migration inicializa a versão `user-entered-arithmetic`.
- A última execução PHPUnit conhecida iniciou 15 testes e teve 15 erros antes das asserções por `Unregistered skills_edu schema exists; reconcile before migration.`. `phpunit.xml` fixa `DB_DATABASE`, mas não host/porta/credenciais; por isso não foi repetida nem considerada evidência de API estável.
- Nenhuma base de dados ou serviço externo foi contactado nesta execução. Não foram alteradas migrations, código do motor, API ou dependências.
## Ficheiros e rollback
- Criado: `docs/investmoz/simulations/stage-10-gate.md`.
- Sem alterações em código, testes, OpenAPI, migrations ou dados.
- Rollback documental: remover apenas este ficheiro. Não há rollback de schema ou dados a executar.
**Gate inicial:** BLOQUEADO antes da aprovação funcional registada abaixo; consultar as actualizações posteriores para o estado vigente.
## Actualização — fórmulas funcionais fornecidas pelo utilizador (2026-10-01)
Foram fornecidas expressões funcionais para lucro tributável e IRPC estimado, IVA liquidado/a pagar, ISPC, IRPS por escalões, contribuições INSS, retenções, valor aduaneiro, direitos, ICE, IVA de importação, custo fiscal total e taxa efectiva. A aprovação funcional foi registada na actualização de 2026-10-01; continuam ausentes valores versionados, fontes oficiais e assinaturas nominais de revisão.
### Regras e limites registados
- IRPC: `LT = rendimentos tributáveis - gastos fiscalmente dedutíveis + correcções positivas - correcções negativas - prejuízos dedutíveis`; `IRPC bruto = max(0, LT) × taxa_IRPC`; pagamentos por conta, benefícios e créditos são linhas separadas. Elegibilidade, limites e taxa dependem de parâmetros/fontes vigentes.
- IVA: separar operações tributadas, isentas, taxa zero e fora do âmbito. Dedutibilidade, crédito anterior e componentes da base precisam de dados/justificação e parâmetros versionados.
- ISPC e IRPS: dependem de elegibilidade/regime, tabela e escalões, periodicidade, deduções e dados estritamente necessários; sem esses dados, resultado indeterminado.
- INSS: os valores indicados de 4% empregador e 3% trabalhador estão expressamente sujeitos a validação; não são tratados como taxas aprovadas. TCP requer parâmetro próprio.
- Retenções: requerem natureza do pagamento, residência, convenção aplicável e documentação para eventual taxa convencional.
- Cálculos aduaneiros (VA, direitos, ICE e IVA de importação) pertencem à Etapa 11; código pautal, origem, preferência, método cambial, fonte e componentes de base continuam obrigatórios. A sua inclusão no material recebido não inicia a Etapa 11.
- Custo fiscal total e taxa efectiva exigem classificação de recuperabilidade e selecção explícita da base económica; retenções e descontos de trabalhador devem ficar separados conforme especificado.
### Estado do gate após a actualização
**Continua BLOQUEADO para implementação e cálculo fiscal.** A aprovação funcional declarada pelo utilizador está registada. Permanecem pendentes: catálogo de parâmetros de produção por imposto com `scope_key`, vigência, fonte oficial e hash; escalas, modos e momentos de arredondamento; contrato estável de API/snapshot fiscal; demonstração de RLS/IAM; e confirmação de base descartável isolada para testes. As taxas de teste não podem preencher o catálogo de produção. Qualquer cenário sem parâmetro aplicável deve ser explicitamente indeterminado.
Esta actualização é documental. Não foram alterados motor, API, OpenAPI, migrations, dependências ou dados; a Etapa 11 não foi iniciada.
## Actualização — critérios de validação e aceitação fornecidos pelo utilizador (2026-10-01)
Critérios recebidos para os gates do motor e dos cálculos relacionados:
- Rejeitar montantes negativos e percentagens fora de `[0, 100]`; reconciliar somas de financiamento com o investimento ou sinalizar a divergência.
- Para alfândegas, exigir código pautal para cálculo definitivo. Sem classificação, permitir apenas estimativa explicitamente incompleta e solicitar classificação; não seleccionar taxa genérica.
- Só aplicar benefícios quando regime, certificado, período e critérios estejam satisfeitos. Caso contrário, exibi-los como oportunidade a validar, sem os incluir como crédito calculado.
- Em cada linha calculada, expor fórmula, valores substituídos, versão da regra, vigência e fundamento legal.
- Cobrir em casos de teste limites de escalões, mudanças de taxa por data, zero, valores elevados, isenções, arredondamento e combinações incompatíveis.
- Reconciliar totais do relatório com a resposta da API e com o snapshot persistido.
- Impedir uso em produção de regra vencida, sem fonte ou sem aprovação.
Estes critérios complementam a especificação funcional. A aprovação funcional posterior está registada abaixo; estes critérios não comprovam fontes legais, parâmetros vigentes, arredondamento, API/snapshot, RLS ou ambiente de testes. Nenhuma regra incompleta ou não aprovada pode ser usada em produção.
## Actualização — matriz de casos e Go/No-Go recebida (2026-10-01)
Foi recebido um conjunto de casos com resultados esperados, inicialmente registado como proposta de casos dourados. Em 2026-10-01, o utilizador instruiu que a matriz e todas as regras fornecidas fossem consideradas formalmente aprovadas. Regista-se essa aprovação funcional declarada pelo utilizador, sem atribuir assinaturas nominais de Fiscalista/QA que não foram fornecidas. Os valores explicitamente chamados “taxa teste” continuam a ser fixtures sintéticas de QA e não podem ser publicados nem tratados como taxas legais.
### Casos funcionais recebidos
- IRPC: `IRPC-01` a `IRPC-04` cobrem LT 400 000 e imposto 128 000 com taxa de teste 32%, prejuízo e imposto zero, crédito/benefício elegível (93 000) e certificado ausente (benefício excluído; 113 000 após créditos).
- IVA: `IVA-01` a `IVA-04` cobrem liquidado 16 000/pagar 8 000 com taxa de teste 16%, exportação a taxa zero, crédito estimado de 3 500 e compra sem direito a dedução.
- ISPC/IRPS/INSS: `ISPC-01`/`02` testam elegibilidade e limite; `IRPS-01`/`02` testam fronteiras de escalões; `INSS-01` testa 4%/3% sintéticos e `INSS-02` exclui componente não contributiva.
- Retenções: `RET-01` a `RET-03` testam taxa doméstica de 20%, recusa de convenção sem certificado e taxa convencional sintética de 10% com documentação.
- Alfândega/importação: `ADU-01`/`02` esperam VA 80 000 MZN e direitos 16 000; `ADU-03` bloqueia direitos sem código pautal; `ICE-01`/`02` testam ad valorem e taxa por unidade; `IMP-IVA-01` espera base 102 000 e IVA de importação 16 320.
- Agregados: `CFT-01` espera 118 000 excluindo IVA recuperável de 16 320; `TEF-01` espera 29,50% sobre lucro 400 000.
### Testes transversais e critérios de release recebidos
Foram também fornecidos `TMP-01/02` (vigência e sobreposição), `RND-01` (arredondamento), `CUR-01` (câmbio), `RULE-01/03` (fonte, whitelist e ciclos), `API-01/03` (idempotência e ETag), `SEC-01/02` (isolamento e segregação de funções), `AUD-01` (auditoria), `REP-01` (reconciliação PDF/JSON), `PERF-01` (p95 < 2 s para 100 regras/500 parâmetros) e `ACC-01` (teclado/acessibilidade). Go exige 100% dos críticos, pelo menos 95% do conjunto, zero vulnerabilidades críticas/altas abertas, reconciliação, assinaturas, rollback, backup e observabilidade; os No-Go também estão registados conforme a matriz fornecida.
Esta matriz fornece resultados de teste reproduzíveis para vários cenários e fica aprovada funcionalmente conforme a instrução do utilizador. Não define fontes legais oficiais, taxas/limites/parâmetros de produção para todos os cenários, política de arredondamento (ponto, escala e modo), versões efectivas do conjunto de regras nem identidade/assinatura individual dos revisores. Inclui cenários de alfândega, segurança, relatórios e release, que pertencem a gates posteriores e não iniciam essas etapas.
**Estado do gate após esta entrada: BLOQUEADO por dependências técnicas e parâmetros de produção em falta.** A aprovação funcional da matriz/regras foi registada. Permanecem necessários: pacote de parâmetros de produção com fontes oficiais, vigências e hash; decisão explícita de escala, modo e momento de arredondamento; contrato/API e snapshots fiscais estáveis; demonstração dos controlos IAM/RLS; e confirmação de ambiente isolado de testes. As taxas designadas “teste” permanecem apenas em fixtures. Nenhum teste de motor foi executado porque o motor e o pacote de produção ainda não existem.
## Reavaliação do gate — pedido de execução da Etapa 10 (2026-10-01)
### Plano condicionado e ficheiros previstos após desbloqueio
1. Formalizar armazenamento e resolução temporal dos parâmetros/fontes por `scope_key`, sem incluir taxas sintéticas em produção.
2. Implementar AST allowlist e validação de DAG acíclico; calcular com decimal exacto e política de arredondamento explícita; devolver indeterminação quando faltar parâmetro/entrada.
3. Integrar execução fiscal, explicabilidade, proveniência e snapshots imutáveis no fluxo de simulação já existente; actualizar contrato OpenAPI e cobrir casos dourados.
4. Executar testes unitários e de integração somente em base descartável confirmada.
Ficheiros identificados pela estrutura actual para uma futura alteração: criar `app/Domain/Simulations/Tax/RuleAstValidator.php`, `app/Domain/Simulations/Tax/RuleDag.php`, `app/Domain/Simulations/Tax/TaxRuleEngine.php`, `app/Domain/Simulations/Tax/ExactDecimal.php`, `app/Domain/Simulations/Tax/TaxParameterResolver.php`, `app/Models/TaxRuleSet.php`, `app/Models/TaxParameter.php`, migration aditiva `database/migrations/2026_10_02_000100_create_tax_rule_sets_and_parameters.php`, testes `tests/Unit/Domain/Simulations/Tax/RuleAstValidatorTest.php`, `RuleDagTest.php`, `TaxRuleEngineTest.php`, `ExactDecimalTest.php`, `TaxParameterResolverTest.php` e `tests/Feature/SimulationTaxExecutionTest.php`; alterar `app/Jobs/RunSimulation.php`, `app/Services/SimulationExecution.php`, `docs/investmoz/simulations/openapi.json` e, se o esquema aprovado exigir campos fiscais novos, `app/Models/SimulationResult.php`/`SimulationResultSource.php`. PHP 8.5.11 nesta máquina tem `bcmath` e `pdo_pgsql` disponíveis, mas o contrato de arredondamento continua por aprovar. Esta lista é uma proposta condicionada, não autorização para criar schema antes da aprovação do catálogo e arredondamento.
### Resultado da reavaliação
- Stack confirmada nesta execução: Laravel 13.30.1, PHP CLI 8.5.11, React 19/Inertia; Composer define PHP `^8.3`, PHPUnit `^12.5.23`; contrato OpenAPI de simulação é 3.1.0 e descreve apenas aritmética indicativa. As migrations e CI usam PostgreSQL/PostGIS.
- `RunSimulation` executa actualmente a fórmula genérica via `numeric`, guarda snapshots de entrada/resultado e fonte `user_input`; não resolve regra/fonte fiscal. `SimulationExecution` mantém idempotência, transacção e `If-Match` para cenários.
- ADR-004 permite somente AST allowlist futura e não define regras/parâmetros. ADR-006 deixa escala/modo/momento de arredondamento por aprovar. ADR-007 mantém IAM/RLS bloqueado até matriz de papéis e contexto ser aprovada.
- PHPUnit permanece sem evidência verde para este escopo: a última execução documentada falhou antes das asserções devido ao schema `skills_edu`; `phpunit.xml` só fixa `DB_DATABASE=investmoz_test`, sem confirmar endpoint descartável. Não repeti a suite nem contactei base de dados.
- O valor de teste 32%, 16%, 3%/4% e os demais valores sintéticos dos casos dourados não são parâmetros legais de produção. A API fiscal também não está estabilizada como contrato versionado.
**Decisão: parar sem alterações de código, migrations ou testes.** A pré-condição de parâmetros aprovados para produção/API/snapshot não está satisfeita. Para desbloquear, fornecer catálogo de produção com fonte legal oficial, hash, `scope_key` e vigência; configuração aprovada de arredondamento; e confirmação do ambiente isolado e da matriz IAM/RLS. As fórmulas e a matriz já declaradas aprovadas não são reabertas por este bloqueio.
@@ -0,0 +1,39 @@
# Etapa 11 — Aduanas, câmbio e cenários
**Estado proposto: BLOQUEADO — não iniciada.** A Etapa 11 depende da aprovação/conclusão do motor principal e das fontes legais da Etapa 10. A Etapa 10 continua bloqueada por falta de parâmetros de produção versionados com fontes oficiais, arredondamento definido e gates técnicos/API/RLS/testes por demonstrar.
## Pré-condições e evidência disponível
| Pré-condição | Evidência disponível | Estado |
| --- | --- | --- |
| Motor principal aprovado | O gate da Etapa 10 regista fórmulas e matriz aprovadas funcionalmente pelo utilizador, mas nenhum motor foi implementado nem testado | BLOQUEADO |
| Fontes legais e parâmetros aduaneiros | Foram fornecidas fórmulas e casos sintéticos (`ADU-01` a `ADU-03`, `ICE-01`/`02`, `IMP-IVA-01`); não incluem pauta/catálogo oficial versionado, fontes, vigências ou hashes de parâmetros de produção | BLOQUEADO |
| Câmbio rastreável | `CUR-01` especifica que valor original, taxa, data e fonte devem ser guardados; não define fornecedor/fonte oficial, método nem regra de selecção da taxa | BLOQUEADO |
| Classificação e preferência | A regra fornecida exige código pautal e prova de origem/preferência; não foi fornecido catálogo nem conjunto de documentos/critérios oficiais | BLOQUEADO |
| Ambiente e controlos técnicos | Gate da Etapa 10 ainda não demonstra contrato/API, snapshots fiscais, RLS/IAM nem ambiente isolado de testes | BLOQUEADO |
## Regras já especificadas funcionalmente
- Valor aduaneiro: factura convertida + frete + seguro + ajustamentos incluíveis, conforme método/câmbio vigentes.
- Direitos: `VA × taxa_pautal`; sem código pautal, direitos ficam bloqueados e não se usa taxa genérica. Pode existir apenas estimativa explicitamente incompleta, sem direitos calculados.
- ICE: base e taxa específica/ad valorem dependem do produto, unidade e pauta vigente.
- IVA de importação: base e componentes são parâmetros versionados e citados.
- Preferência é condicional a origem/documentos; o snapshot deve conservar taxa cambial, data, fonte e método.
As taxas de teste do anexo são fixtures de QA e não valores legais. Os casos de Etapa 11 aprovados funcionalmente (`ADU-*`, `ICE-*`, `IMP-IVA-01`, `CUR-01`) ainda precisam de fontes legais e parâmetros de produção associados.
## Condição para retomar
1. Desbloquear e concluir a Etapa 10, incluindo fonte legal/versão do motor e respetivos snapshots.
2. Fornecer catálogos aduaneiros oficiais/versionados: pauta/código, direitos, ICE, base do IVA de importação, origem e preferências, com vigência e proveniência.
3. Aprovar método cambial, fonte, data aplicável, conversão e política de arredondamento.
4. Confirmar documentos/provas necessários para classificação e preferência, incluindo resposta incompleta quando faltarem dados.
5. Confirmar ambiente isolado, IAM/RLS e contratos de API/snapshot; então inspeccionar o repositório e propor plano e lista exacta de ficheiros antes de implementar.
## Evidência desta execução
- Consultados `docs/investmoz/simulations/stage-10-gate.md` e os documentos de implementação disponíveis; não existe gate anterior da Etapa 11.
- Não foram alterados código, migrations, API, dependências ou dados. Nenhum teste foi executado. A Etapa 11 não foi iniciada.
- Rollback: remover apenas este documento, caso a sua criação seja revertida; sem rollback de schema/dados.
**Decisão proposta: manter BLOQUEADO e parar na Etapa 11 até todas as pré-condições acima terem evidência verificável.**
@@ -0,0 +1,34 @@
# Etapa 9 — Formulário React com dropdowns
**Data:** 2026-10-01
**Aprovação:** o utilizador autorizou continuar e assumir as aprovações anteriores.
**Estado:** IMPLEMENTAÇÃO PARCIAL — formulário, validação e autosave seguro implementados; submissão formal, anexos e validação manual assistiva permanecem fora do gate concluído.
## Implementado
- A rota `/Simulador` apresenta um formulário React em etapas para finalidade, perfil, identificação, projecto, localização, finanças, impacto, cenário fiscal aritmético, oportunidade, documentos aplicáveis e revisão.
- Dropdowns pesquisáveis usam catálogos publicados; actividade é filtrada pelo sector. A navegação e selecção múltipla suportam teclado, com limpeza dos valores descendentes quando muda uma opção-pai.
- Campos condicionais têm validação inline por etapa, incluindo limites aritméticos e reconciliação das fontes de financiamento. A API exige base, taxa e redução informadas explicitamente; nenhum valor fiscal é predefinido.
- O rascunho é guardado com debounce no servidor, cifrado por snapshot, isolado por utilizador autenticado ou token anónimo e protegido com ETag/If-Match, chave idempotente, limitação de pedidos e RLS PostgreSQL. A cópia local de sessão exclui identidade e contactos.
- A API valida chaves permitidas, tipos, limites, actividade publicada correspondente ao sector, hierarquia territorial e reconciliação financeira. A migration é aditiva e a reversão recusa apagar rascunhos existentes.
- O cenário fiscal continua a usar a API aritmética existente; o formulário não submete uma manifestação nem publica dados.
## Limites conhecidos
- A etapa Documentos recolhe apenas os tipos aplicáveis a preparar. Ainda não há upload, verificação ou armazenamento de ficheiros.
- Os campos de salários, pagamentos externos, importações e benefícios alegados são recolhidos como texto; não há subformulários estruturados nem validação fiscal desses dados.
- A lista de formas jurídicas e de zonas especiais ainda não tem catálogo publicado. A forma jurídica está como texto e a referência da zona/projecto é texto livre. Países dependem do catálogo territorial disponível.
- A etapa Revisão recolhe declarações e autorizações, mas não as envia como manifestação formal. A submissão e o fluxo de tratamento pertencem à etapa seguinte.
- O formulário tem atributos semânticos e suporte de teclado implementado, mas a verificação manual com leitor de ecrã e em dispositivos móveis precisa de ser feita num navegador.
## Verificações
- `npm.cmd run types:check` — aprovado.
- `npm.cmd run build` — aprovado; o Vite avisou que o pacote opcional `fontaine` não está instalado, sem impedir o build.
- `node --experimental-strip-types --test tests/frontend/simulation-form.test.mjs` — 6 testes aprovados.
- `php.exe -l` — sintaxe aprovada no FormRequest, serviço, controller, model e migration novos.
- PHPUnit não executado: o PHP disponível é 8.1.10 e a versão instalada do PHPUnit exige PHP >= 8.3. Os testes de feature da API/migration continuam por executar num runtime compatível.
## Rollback
Reverter apenas os ficheiros listados pelo diff desta etapa. Para reverter a migration de rascunhos, primeiro eliminar os dados de rascunho por procedimento aprovado; a migration recusa rollback enquanto houver registos. Preservar todas as alterações locais preexistentes.
@@ -1,5 +1,6 @@
import { useEffect, useId, useState } from "react";
import { api } from "@/pages/investment/shared";
import { replaceTerritoryPath } from "@/lib/territory-path.js";
export type TerritoryOption = {
id: string;
@@ -18,7 +19,7 @@ const emptyTerritoryPath: TerritoryOption[] = [];
export function ProvinceSelect({
value,
onChange,
endpoint = "/api/v1/investment/territory-options",
endpoint = "/api/v1/catalogs/jurisdictions",
placeholder = "Seleccione uma província",
className = control,
label = "Província",
@@ -94,7 +95,7 @@ export function ProvinceSelect({
export function TerritoryFields({
initial = emptyTerritoryPath,
onChange,
endpoint = "/api/v1/investment/territory-options",
endpoint = "/api/v1/catalogs/jurisdictions",
}: {
initial?: TerritoryOption[];
onChange?: (path: TerritoryOption[]) => void;
@@ -114,7 +115,7 @@ export function TerritoryFields({
selected={path[index]}
endpoint={endpoint}
onChange={(value) => {
const next = [...path.slice(0, index), ...(value ? [value] : [])];
const next = replaceTerritoryPath(path, index, value);
setPath(next);
onChange?.(next);
}}
@@ -285,7 +286,7 @@ export default function InvestmentTerritory({ kind, id }: { kind: string; id: st
parent={path[index - 1]?.id ?? ""}
selected={path[index]}
onChange={(value) => {
setPath([...path.slice(0, index), ...(value ? [value] : [])]);
setPath(replaceTerritoryPath(path, index, value));
setMessage("");
}}
/>
+447
View File
@@ -0,0 +1,447 @@
import { useEffect, useId, useMemo, useRef, useState } from "react";
import {
getAutosaveSnapshot,
partitionSimulationDraft,
restoreSimulationDraft,
replaceSimulationValue,
simulationFieldGroups,
simulationDraftFingerprint,
simulationOptions,
simulationSections,
validateSimulationDraft,
type SimulationDraft,
} from "@/lib/simulation-form";
type Choice = { id: string; label: string };
const draftKey = "investmoz-simulation-draft-v1";
const tokenKey = "investmoz-simulation-token-v1";
const idempotencyStorageKey = "investmoz-simulation-idempotency-v1";
const draftCreateKey = "investmoz-simulation-draft-create-key-v1";
const draftIdKey = "investmoz-simulation-draft-id-v1";
const idempotencyKey = () => crypto.randomUUID();
function getSimulationToken(): string {
let token = sessionStorage.getItem(tokenKey);
if (!token) {
token = crypto.randomUUID().replaceAll("-", "") + crypto.randomUUID().replaceAll("-", "");
sessionStorage.setItem(tokenKey, token);
}
return token;
}
function csrfToken(): string {
return decodeURIComponent(document.cookie.split("; ").find(cookie => cookie.startsWith("XSRF-TOKEN="))?.slice(11) ?? "");
}
function labelFor(value: string): string {
const words: Record<string, string> = {
simulate_only: "Apenas simular", own_investment: "Investimento próprio", seek_investor: "Procurar investidor",
seek_partner: "Procurar parceiro", present_opportunity: "Apresentar oportunidade", request_facilitation: "Solicitar facilitação",
investment: "Investimento", business: "Negócio", singular: "Pessoa singular", collective: "Pessoa colectiva",
sole_trader: "Empresário em nome individual", consortium: "Consórcio", national: "Nacional", foreign: "Estrangeiro",
mixed: "Misto", existing: "Existente", being_formed: "Em constituição", "pt-MZ": "Português (Moçambique)", en: "English",
portal: "Portal", email: "Correio electrónico", phone: "Telefone", new: "Novo projecto", expansion: "Expansão",
acquisition: "Aquisição", partnership: "Parceria", concession: "Concessão", supplier: "Fornecedor", buyer: "Comprador",
other: "Outro", idea: "Ideia", concept: "Conceito", pre_feasibility: "Pré-viabilidade", feasibility: "Viabilidade",
financing: "Financiamento", implementation: "Implementação", operation: "Operação", owned: "Propriedade",
duat: "DUAT", leased: "Arrendado", requested: "Solicitado", unknown: "Por determinar", MZN: "MZN", USD: "USD",
EUR: "EUR", ZAR: "ZAR", A: "Categoria A", B: "Categoria B", C: "Categoria C", por_determinar: "Por determinar",
offer: "Oferta", demand: "Procura", both: "Oferta e procura", capital: "Capital", partner: "Parceiro",
land: "Terreno", technology: "Tecnologia", licence: "Licença", services: "Serviços", public: "Público",
registered_users: "Utilizadores registados", restricted: "Restrito", confidential: "Confidencial", yes: "Sim", no: "Não",
energy: "Energia",
};
return words[value] ?? value.replaceAll("_", " ");
}
function useChoices(source: string | undefined, query: string, parent: string, enabled: boolean) {
const [choices, setChoices] = useState<Choice[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const staticChoices = useMemo(() => {
if (source === "currency") {
const intl = Intl as unknown as { supportedValuesOf?: (key: string) => string[] };
return (intl.supportedValuesOf?.("currency") ?? []).map(code => ({ id: code, label: code }));
}
if (!source || ["country", "province", "district", "admin_post", "locality", "sector", "activity"].includes(source)) return [];
return (simulationOptions[source] ?? []).map(value => ({ id: value, label: labelFor(value) }));
}, [source]);
useEffect(() => {
if (!enabled || !source || !["country", "province", "district", "admin_post", "locality", "sector", "activity"].includes(source)) {
setChoices(staticChoices);
setError("");
return;
}
if (["district", "admin_post", "locality", "activity"].includes(source) && !parent) {
setChoices([]);
return;
}
const controller = new AbortController();
const timer = window.setTimeout(async () => {
setLoading(true);
setError("");
try {
let url: URL;
if (source === "sector") {
url = new URL("/api/v1/sectors", window.location.origin);
} else {
url = new URL("/api/v1/catalogs/" + (source === "activity" ? "economic-activities" : "jurisdictions"), window.location.origin);
url.searchParams.set("q", query);
url.searchParams.set("per_page", "50");
if (source === "activity") url.searchParams.set("sector_id", parent);
else {
url.searchParams.set("type", source === "country" ? "country" : source);
if (parent) url.searchParams.set("parent_id", parent);
}
}
const response = await fetch(url, { signal: controller.signal, headers: { Accept: "application/json" }, credentials: "same-origin" });
if (!response.ok) throw new Error("Não foi possível carregar as opções. Tente novamente.");
const payload = await response.json();
const rows = payload.data ?? [];
setChoices(rows.map((row: Record<string, unknown>) => ({
id: String(row.id ?? row.value ?? ""),
label: String(row.label ?? row.name ?? ""),
})).filter((row: Choice) => row.id && row.label));
} catch (cause) {
if (!controller.signal.aborted) setError(cause instanceof Error ? cause.message : "Erro ao carregar opções.");
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}, 180);
return () => { window.clearTimeout(timer); controller.abort(); };
}, [source, query, parent, enabled, staticChoices]);
return { choices, loading, error, retry: () => setError("") };
}
function SearchChoice({
label, source, parent = "", value, selectedLabel, multiple, disabled, error, required, onChange,
}: {
label: string; source?: string; parent?: string; value: string | string[]; selectedLabel?: string | string[];
multiple?: boolean; disabled?: boolean; error?: string; required?: boolean; onChange: (value: string | string[], text?: string | string[]) => void;
}) {
const generated = useId();
const inputId = `choice-${generated}`;
const listId = `${inputId}-options`;
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const [active, setActive] = useState(-1);
const { choices, loading, error: loadError } = useChoices(source, query, parent, !disabled && open);
const chosen = Array.isArray(value) ? value : value ? [value] : [];
const filtered = choices.filter(option => option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()) || option.id.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
const describedBy = [error ? `${inputId}-error` : "", loadError ? `${inputId}-load-error` : ""].filter(Boolean).join(" ") || undefined;
return <div className="space-y-1">
<label htmlFor={inputId} className="block text-sm font-medium">{label}</label>
<input id={inputId} role="combobox" aria-autocomplete="list" aria-expanded={open} aria-controls={listId}
aria-activedescendant={open && filtered[active] ? `${listId}-${active}` : undefined}
aria-invalid={Boolean(error || loadError)} aria-required={required || undefined} aria-describedby={describedBy} autoComplete="off"
className="w-full rounded-lg border border-slate-300 bg-white p-3 text-slate-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-700 disabled:bg-slate-100"
value={open ? query : multiple ? "" : selectedLabel || (chosen[0] ? choices.find(o => o.id === chosen[0])?.label ?? chosen[0] : "")}
placeholder={disabled ? "Seleccione primeiro o campo anterior" : "Pesquisar opções…"} disabled={disabled}
onFocus={() => { setOpen(true); setQuery(""); }}
onBlur={() => { window.setTimeout(() => setOpen(false), 120); }}
onChange={event => { setQuery(event.target.value); setActive(0); setOpen(true); }}
onKeyDown={event => {
if (event.key === "ArrowDown") { event.preventDefault(); setOpen(true); setActive(i => Math.min(i + 1, filtered.length - 1)); }
if (event.key === "ArrowUp") { event.preventDefault(); setActive(i => Math.max(0, i - 1)); }
if (event.key === "Enter" && open && filtered[active]) { event.preventDefault(); const option = filtered[active]; if (multiple) { if (!chosen.includes(option.id)) onChange([...chosen, option.id], [...(Array.isArray(selectedLabel) ? selectedLabel : []), option.label]); setQuery(""); } else { onChange(option.id, option.label); setQuery(""); setOpen(false); } }
if (event.key === "Escape") { setOpen(false); setQuery(""); }
if (event.key === "Backspace" && multiple && query === "" && chosen.length) onChange(chosen.slice(0, -1), Array.isArray(selectedLabel) ? selectedLabel.slice(0, -1) : []);
}} />
{multiple && chosen.length > 0 && <ul aria-label={`Opções seleccionadas: ${label}`} className="flex flex-wrap gap-2">
{chosen.map((item, index) => { const text = Array.isArray(selectedLabel) ? selectedLabel[index] : choices.find(option => option.id === item)?.label ?? item; return <li key={item} className="flex items-center gap-1 rounded bg-emerald-50 px-2 py-1 text-sm">
{text}
<button type="button" aria-label={`Remover ${text}`} onClick={() => onChange(chosen.filter((_, i) => i !== index), Array.isArray(selectedLabel) ? selectedLabel.filter((_, i) => i !== index) : [])} className="rounded px-1 focus-visible:outline-2">×</button>
</li>; })}
</ul>}
{open && !disabled && <ul id={listId} role="listbox" aria-label={label} aria-multiselectable={multiple || undefined} className="max-h-56 overflow-auto rounded border bg-white shadow-lg">
{loading && <li role="status" className="p-3 text-sm">A carregar opções…</li>}
{!loading && filtered.map((option, index) => <li id={`${listId}-${index}`} key={option.id} role="option" aria-selected={chosen.includes(option.id)}
className={`cursor-pointer p-3 text-sm hover:bg-emerald-50 ${active === index ? "bg-emerald-50" : ""}`}
onMouseEnter={() => setActive(index)} onMouseDown={event => event.preventDefault()}
onClick={() => { if (multiple) { if (!chosen.includes(option.id)) onChange([...chosen, option.id], [...(Array.isArray(selectedLabel) ? selectedLabel : []), option.label]); setQuery(""); } else { onChange(option.id, option.label); setQuery(""); setOpen(false); } }}>
{option.label}{chosen.includes(option.id) ? " ✓" : ""}
</li>)}
{!loading && filtered.length === 0 && <li className="p-3 text-sm">Sem opções correspondentes.</li>}
</ul>}
{loadError && <p id={`${inputId}-load-error`} role="alert" className="text-sm text-red-800">{loadError}</p>}
{error && <p id={`${inputId}-error`} role="alert" className="text-sm text-red-800">{error}</p>}
</div>;
}
export function SimulationForm() {
const [draft, setDraft] = useState<SimulationDraft>({});
const [ready, setReady] = useState(false);
const [step, setStep] = useState(0);
const [errors, setErrors] = useState<Record<string, string>>({});
const [saveStatus, setSaveStatus] = useState("Rascunho ainda não guardado.");
const [serverError, setServerError] = useState("");
const [isCalculating, setIsCalculating] = useState(false);
const [hasSaveConflict, setHasSaveConflict] = useState(false);
const [result, setResult] = useState<Record<string, unknown> | null>(null);
const calculationInFlightRef = useRef(false);
const draftIdRef = useRef<string | null>(null);
const draftEtagRef = useRef<string | null>(null);
const lastSavedFingerprintRef = useRef<string | null>(null);
const saveQueueRef = useRef<Promise<void>>(Promise.resolve());
const saveConflictRef = useRef(false);
const current = simulationSections[step];
const fields = (simulationFieldGroups[current.id] ?? []).filter(field => !field.showWhen || field.showWhen(draft));
useEffect(() => {
let cancelled = false;
void (async () => {
let local: SimulationDraft = {};
try {
const stored = sessionStorage.getItem(draftKey);
if (stored) local = JSON.parse(stored) as SimulationDraft;
} catch { setSaveStatus("Não foi possível recuperar o rascunho desta sessão."); }
try {
const response = await fetch("/api/v1/simulation-drafts/current", {
credentials: "same-origin", headers: { Accept: "application/json", "X-Simulation-Token": getSimulationToken() },
});
if (response.ok) {
const payload = await response.json();
if (payload.data) {
draftIdRef.current = payload.data.id;
draftEtagRef.current = response.headers.get("ETag");
sessionStorage.setItem(draftIdKey, payload.data.id);
const restored = restoreSimulationDraft(payload.data.economic_snapshot, payload.data.personal_snapshot);
lastSavedFingerprintRef.current = simulationDraftFingerprint(restored);
setDraft({ ...restored, ...local });
setSaveStatus("Rascunho recuperado do servidor.");
} else if (Object.keys(local).length) {
const storedId = sessionStorage.getItem(draftIdKey);
if (storedId) sessionStorage.removeItem(draftIdKey);
setDraft(local);
}
} else if (Object.keys(local).length) setDraft(local);
} catch {
if (Object.keys(local).length) setDraft(local);
setSaveStatus("Sem ligação ao servidor; o rascunho desta sessão será mantido localmente.");
}
if (!cancelled) setReady(true);
})();
return () => { cancelled = true; };
}, []);
useEffect(() => {
if (!ready || !draft.purpose) return;
const timer = window.setTimeout(() => {
try {
sessionStorage.setItem(draftKey, JSON.stringify(getAutosaveSnapshot(draft)));
}
catch { setSaveStatus("Não foi possível guardar o rascunho neste navegador."); }
if (saveConflictRef.current) return;
const snapshots = partitionSimulationDraft(draft);
const fingerprint = simulationDraftFingerprint(draft);
if (fingerprint === lastSavedFingerprintRef.current) return;
setSaveStatus("A guardar o rascunho no servidor…");
saveQueueRef.current = saveQueueRef.current.then(async () => {
const token = getSimulationToken();
const headers = { Accept: "application/json", "Content-Type": "application/json", "X-XSRF-TOKEN": csrfToken(), "X-Simulation-Token": token };
if (!draftIdRef.current) {
const currentResponse = await fetch("/api/v1/simulation-drafts/current", { credentials: "same-origin", headers: { Accept: "application/json", "X-Simulation-Token": token } });
const currentPayload = currentResponse.ok ? await currentResponse.json() : { data: null };
if (currentPayload.data) {
draftIdRef.current = currentPayload.data.id;
draftEtagRef.current = currentResponse.headers.get("ETag");
sessionStorage.setItem(draftIdKey, currentPayload.data.id);
}
}
let response: Response;
if (draftIdRef.current && draftEtagRef.current) {
response = await fetch(`/api/v1/simulation-drafts/${draftIdRef.current}`, {
method: "PUT", credentials: "same-origin", headers: { ...headers, "If-Match": draftEtagRef.current }, body: JSON.stringify(snapshots),
});
} else {
let key = sessionStorage.getItem(draftCreateKey);
if (!key) { key = idempotencyKey(); sessionStorage.setItem(draftCreateKey, key); }
response = await fetch("/api/v1/simulation-drafts", {
method: "POST", credentials: "same-origin", headers: { ...headers, "Idempotency-Key": key }, body: JSON.stringify(snapshots),
});
}
if (response.status === 412 || response.status === 428) {
saveConflictRef.current = true;
setHasSaveConflict(true);
setSaveStatus("O rascunho mudou noutro separador. Recarregue para rever a versão mais recente.");
return;
}
const payload = await response.json();
if (!response.ok) throw new Error(payload.message ?? "Não foi possível guardar o rascunho no servidor.");
draftIdRef.current = payload.data.id;
draftEtagRef.current = response.headers.get("ETag");
lastSavedFingerprintRef.current = fingerprint;
sessionStorage.setItem(draftIdKey, payload.data.id);
sessionStorage.removeItem(draftCreateKey);
setSaveStatus(`Rascunho guardado no servidor às ${new Intl.DateTimeFormat("pt-MZ", { timeStyle: "short" }).format(new Date())}.`);
}).catch(cause => {
setSaveStatus(cause instanceof Error ? cause.message : "Erro ao guardar o rascunho no servidor.");
});
}, 450);
return () => window.clearTimeout(timer);
}, [draft, ready]);
function change(key: string, value: string | string[], label?: string | string[]) {
setDraft(currentDraft => {
const next = replaceSimulationValue(currentDraft, key, value);
if (label) next[`${key}_label`] = label;
return next;
});
setErrors(current => { const next = { ...current }; delete next[key]; return next; });
setResult(null);
}
async function clearDraft() {
await saveQueueRef.current;
if (draftIdRef.current && draftEtagRef.current) {
const response = await fetch(`/api/v1/simulation-drafts/${draftIdRef.current}`, {
method: "DELETE", credentials: "same-origin",
headers: { Accept: "application/json", "X-XSRF-TOKEN": csrfToken(), "X-Simulation-Token": getSimulationToken(), "If-Match": draftEtagRef.current },
});
if (!response.ok) { setSaveStatus("Não foi possível remover o rascunho do servidor. Recarregue e tente novamente."); return; }
}
for (const key of [draftKey, draftIdKey, draftCreateKey, idempotencyStorageKey, tokenKey]) sessionStorage.removeItem(key);
draftIdRef.current = null;
draftEtagRef.current = null;
lastSavedFingerprintRef.current = null;
saveConflictRef.current = false;
setHasSaveConflict(false);
setDraft({});
setResult(null);
setErrors({});
setStep(0);
setSaveStatus("Rascunho removido do servidor e desta sessão.");
}
function navigate(nextStep: number) {
if (nextStep > step) {
for (let index = step; index < nextStep; index++) {
const validation = validateSimulationDraft(draft, simulationSections[index].id);
if (Object.keys(validation).length) {
setStep(index);
setErrors(validation);
window.requestAnimationFrame(() => document.getElementById(`field-${Object.keys(validation)[0]}`)?.focus());
return;
}
}
}
setStep(nextStep);
setErrors({});
document.getElementById("simulator-step-title")?.focus();
}
async function runSimulation() {
if (calculationInFlightRef.current) return;
const validation = validateSimulationDraft(draft, "tax");
setErrors(validation);
setServerError("");
if (draft.tax_calculation !== "yes") return;
if (Object.keys(validation).length) { document.getElementById(`field-${Object.keys(validation)[0]}`)?.focus(); return; }
calculationInFlightRef.current = true;
setIsCalculating(true);
try {
const token = getSimulationToken();
const xsrf = csrfToken();
const body = { base: draft.base, rate: draft.rate, reduction: draft.reduction };
const serialized = JSON.stringify(body);
const previous = sessionStorage.getItem(idempotencyStorageKey);
let key = "";
if (previous) {
try { const saved = JSON.parse(previous) as { payload?: string; key?: string }; if (saved.payload === serialized && saved.key) key = saved.key; } catch { /* replace malformed local state */ }
}
if (!key) { key = idempotencyKey(); sessionStorage.setItem(idempotencyStorageKey, JSON.stringify({ payload: serialized, key })); }
const response = await fetch("/api/v1/simulations", {
method: "POST", credentials: "same-origin",
headers: { "Accept": "application/json", "Content-Type": "application/json", "X-XSRF-TOKEN": xsrf, "Idempotency-Key": key, "X-Simulation-Token": token },
body: serialized,
});
const payload = await response.json();
if (!response.ok) {
if (response.status === 429) {
const retryAfter = Number(response.headers.get("Retry-After") ?? 60);
throw new Error(`Demasiadas tentativas. Aguarde ${retryAfter} segundos antes de voltar a calcular.`);
}
const serverErrors = payload.errors ? Object.values(payload.errors).flat().join(" ") : payload.message;
throw new Error(typeof serverErrors === "string" ? serverErrors : "Não foi possível validar o cenário no servidor.");
}
const simulationId = payload.data?.id;
let simulation = payload.data;
if (simulationId && simulation?.status !== "completed" && simulation?.status !== "failed") {
setSaveStatus("Cenário enviado. A aguardar o cálculo…");
for (let attempt = 0; attempt < 12; attempt++) {
await new Promise(resolve => window.setTimeout(resolve, 1000));
const poll = await fetch(`/api/v1/simulations/${simulationId}`, { credentials: "same-origin", headers: { Accept: "application/json", "X-Simulation-Token": token } });
if (!poll.ok) break;
const pollPayload = await poll.json();
simulation = pollPayload.data;
if (simulation?.status === "completed" || simulation?.status === "failed") break;
}
}
if (JSON.stringify({ base: draft.base, rate: draft.rate, reduction: draft.reduction }) === serialized) setResult(simulation ?? null);
sessionStorage.removeItem(idempotencyStorageKey);
} catch (cause) { setServerError(cause instanceof Error ? cause.message : "Erro de ligação. Tente novamente."); }
finally { calculationInFlightRef.current = false; setIsCalculating(false); }
}
return <section className="space-y-6 rounded-xl border bg-white p-4 sm:p-6" aria-labelledby="simulator-step-title">
<div className="flex flex-wrap items-start justify-between gap-3">
<div><h2 id="simulator-step-title" tabIndex={-1} className="text-xl font-bold outline-none">Formulário de simulação e manifestação</h2>
<p className="mt-1 text-sm text-slate-700">Simulação informativa. Nenhuma taxa legal, elegibilidade ou moeda é presumida.</p></div>
<p role="status" aria-live="polite" className="text-sm">{saveStatus}</p>
</div>
<nav aria-label="Etapas do formulário" className="overflow-x-auto">
<ol className="flex min-w-max gap-2">
{simulationSections.map((section, index) => <li key={section.id}><button type="button" aria-current={step === index ? "step" : undefined}
onClick={() => navigate(index)} className={`rounded-full border px-3 py-2 text-sm focus-visible:outline-2 ${step === index ? "border-emerald-800 bg-emerald-800 text-white" : "bg-white"}`}>
{index + 1}. {section.title}
</button></li>)}
</ol>
</nav>
<div className="h-2 overflow-hidden rounded bg-slate-200" role="progressbar" aria-label="Progresso do formulário" aria-valuemin={0} aria-valuemax={100} aria-valuenow={Math.round(((step + 1) / simulationSections.length) * 100)}>
<div className="h-full bg-emerald-800" style={{ width: `${((step + 1) / simulationSections.length) * 100}%` }} />
</div>
<h3 className="text-lg font-semibold">{current.title}</h3>
<div className="grid gap-4 sm:grid-cols-2">
{fields.map(field => {
const fieldError = errors[field.key];
if (field.options) {
const parentKey = field.key === "district_id" ? "province_id" : field.key === "admin_post_id" ? "district_id" : field.key === "locality_id" ? "admin_post_id" : field.key === "activity_id" ? "sector_id" : "";
return <div key={field.key} id={`field-${field.key}`} tabIndex={-1}>
<SearchChoice label={`${field.label}${field.required ? " (obrigatório)" : ""}`} source={field.options} parent={parentKey ? String(draft[parentKey] ?? "") : ""}
value={draft[field.key] ?? (field.multiple ? [] : "")} selectedLabel={draft[`${field.key}_label`] as string | string[] | undefined} multiple={field.multiple}
disabled={Boolean(parentKey && !draft[parentKey])} error={fieldError} required={field.required}
onChange={(value, text) => change(field.key, value, text)} />
</div>;
}
const isArea = ["area_required"].includes(field.key);
return <div key={field.key}>
<label htmlFor={`field-${field.key}`} className="mb-1 block text-sm font-medium">{field.label}{field.required ? " (obrigatório)" : ""}</label>
{field.type === "textarea" ? <textarea id={`field-${field.key}`} className="min-h-28 w-full rounded-lg border border-slate-300 p-3 focus-visible:outline-2 focus-visible:outline-emerald-700" value={String(draft[field.key] ?? "")} onChange={e => change(field.key, e.target.value)} aria-invalid={Boolean(fieldError)} aria-required={field.required || undefined} aria-describedby={fieldError ? `error-${field.key}` : undefined} />
: <input id={`field-${field.key}`} type={field.type ?? (isArea ? "number" : "text")} min={field.type === "number" ? "0" : undefined} step={field.type === "number" ? "any" : undefined} className="w-full rounded-lg border border-slate-300 p-3 focus-visible:outline-2 focus-visible:outline-emerald-700" value={String(draft[field.key] ?? "")} onChange={e => change(field.key, e.target.value)} aria-invalid={Boolean(fieldError)} aria-required={field.required || undefined} aria-describedby={fieldError ? `error-${field.key}` : undefined} />}
{fieldError && <p id={`error-${field.key}`} role="alert" className="mt-1 text-sm text-red-800">{fieldError}</p>}
{field.key === "reduction" && <p className="mt-1 text-sm text-slate-600">Hipótese sua, não representa um benefício fiscal aprovado.</p>}
</div>;
})}
</div>
{current.id === "financial" && <p className="rounded bg-amber-50 p-3 text-sm">Os montantes são introduzidos pelo utilizador. Confirme que capital, dívida e subvenções reconciliam com o investimento total.</p>}
{current.id === "tax" && <div className="space-y-3 border-t pt-4">
<h4 className="font-semibold">Executar cenário aritmético</h4>
{draft.tax_calculation === "yes" && <button type="button" disabled={isCalculating} aria-busy={isCalculating} className="rounded-lg bg-emerald-800 px-4 py-3 font-semibold text-white focus-visible:outline-2 disabled:cursor-wait disabled:opacity-60" onClick={() => void runSimulation()}>{isCalculating ? "A calcular…" : "Validar e calcular no servidor"}</button>}
{serverError && <p role="alert" className="text-red-800">{serverError}</p>}
{result && <div role="status" className="rounded bg-emerald-50 p-4"><p>Simulação {String(result.status ?? "recebida")}.</p>
{result.status === "completed" && <p>Resultado aritmético indicativo: <strong>{String((result.result_snapshot as Record<string, unknown> | undefined)?.indicative_total ?? "disponível no relatório")}</strong></p>}
{result.status === "failed" && <p role="alert">O cálculo não foi concluído. Reveja os valores e tente novamente.</p>}
{result.status === "queued" || result.status === "running" ? <p className="text-sm">O cálculo continua pendente. Pode consultar o histórico quando estiver disponível.</p> : null}
<p className="text-sm">O resultado é indicativo e não determina taxas legais, elegibilidade ou obrigação fiscal.</p></div>}
</div>}
{hasSaveConflict && <p role="alert" className="rounded border border-amber-700 bg-amber-50 p-3">Este rascunho foi alterado noutro separador. <button type="button" className="font-semibold underline" onClick={() => window.location.reload()}>Recarregar a versão mais recente</button></p>}
<div className="flex flex-wrap justify-between gap-3 border-t pt-4">
<button type="button" disabled={step === 0} onClick={() => navigate(Math.max(0, step - 1))} className="rounded border px-4 py-3 disabled:opacity-50 focus-visible:outline-2">Anterior</button>
{step < simulationSections.length - 1
? <button type="button" onClick={() => navigate(step + 1)} className="rounded-lg bg-emerald-800 px-5 py-3 font-semibold text-white focus-visible:outline-2">Guardar e continuar</button>
: <button type="button" onClick={() => void clearDraft()} className="rounded border px-4 py-3 focus-visible:outline-2">Limpar este rascunho</button>}
</div>
<p className="text-xs text-slate-600">O rascunho é cifrado e guardado no servidor. O navegador mantém apenas o token temporário e os dados não identificativos necessários à recuperação da sessão. A submissão formal ainda não está disponível neste formulário.</p>
</section>;
}
+276
View File
@@ -0,0 +1,276 @@
export type SimulationDraft = Record<string, string | string[]>;
export const simulationSections = [
{ id: "purpose", title: "Finalidade" },
{ id: "promoter", title: "Perfil do promotor" },
{ id: "identity", title: "Identificação e contactos" },
{ id: "project", title: "Projecto ou negócio" },
{ id: "location", title: "Localização" },
{ id: "financial", title: "Estrutura financeira" },
{ id: "impact", title: "Emprego e impacto" },
{ id: "tax", title: "Operação fiscal" },
{ id: "opportunity", title: "Necessidade de negócio" },
{ id: "documents", title: "Documentos" },
{ id: "review", title: "Revisão" },
] as const;
export const simulationOptions: Record<string, string[]> = {
purpose: ["simulate_only", "own_investment", "seek_investor", "seek_partner", "present_opportunity", "request_facilitation"],
manifestation_type: ["investment", "business"],
person_type: ["singular", "collective", "sole_trader", "consortium"],
investor_origin: ["national", "foreign", "mixed"],
entity_stage: ["existing", "being_formed"],
preferred_language: ["pt-MZ", "en"],
preferred_channel: ["portal", "email", "phone"],
project_type: ["new", "expansion", "acquisition", "partnership", "concession", "supplier", "buyer", "other"],
maturity_stage: ["idea", "concept", "pre_feasibility", "feasibility", "financing", "implementation", "operation"],
land_status: ["owned", "duat", "leased", "requested", "unknown"],
environmental_category: ["A", "B", "C", "por_determinar"],
offer_or_demand: ["offer", "demand", "both"],
opportunity_type: ["capital", "partner", "buyer", "supplier", "land", "technology", "licence", "services", "other"],
confidentiality: ["public", "registered_users", "restricted", "confidential"],
preferred_publication: ["yes", "no"],
residency_status: ["resident", "non_resident"],
tax_calculation: ["yes", "no"],
document_types: ["identity_document", "company_registration", "nuit_certificate", "company_profile", "business_plan", "feasibility_study", "proof_of_funds", "special_zone_certificate", "licence", "site_map"],
area_unit: ["m2", "ha", "km2"],
};
export const simulationFieldGroups: Record<string, { key: string; label: string; type?: "number" | "date" | "email" | "textarea"; required?: boolean; options?: string; multiple?: boolean; showWhen?: (draft: SimulationDraft) => boolean }[]> = {
purpose: [
{ key: "purpose", label: "O que pretende fazer?", options: "purpose", required: true },
{ key: "manifestation_type", label: "Tipo de manifestação", options: "manifestation_type", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "preferred_language", label: "Idioma preferido", options: "preferred_language", required: true },
],
promoter: [
{ key: "person_type", label: "Tipo de pessoa", options: "person_type", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "investor_origin", label: "Origem do investidor", options: "investor_origin", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "tax_residency_country", label: "País de residência fiscal", options: "country", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "entity_stage", label: "Situação da entidade", options: "entity_stage", required: true, showWhen: d => d.purpose !== "simulate_only" && ["collective", "consortium"].includes(String(d.person_type)) },
{ key: "legal_form", label: "Forma jurídica (conforme o registo)", required: true, showWhen: d => d.purpose !== "simulate_only" && d.entity_stage === "existing" },
{ key: "nuit", label: "NUIT", showWhen: d => d.purpose !== "simulate_only" },
],
identity: [
{ key: "legal_name", label: "Nome / denominação", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "commercial_name", label: "Nome comercial", showWhen: d => d.purpose !== "simulate_only" },
{ key: "registration_number", label: "Número de registo", required: true, showWhen: d => d.entity_stage === "existing" && d.purpose !== "simulate_only" },
{ key: "representative_name", label: "Nome do representante", required: true, showWhen: d => ["collective", "consortium"].includes(String(d.person_type)) && d.purpose !== "simulate_only" },
{ key: "email", label: "Correio electrónico", type: "email", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "phone", label: "Telefone", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "address", label: "Endereço", type: "textarea", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "preferred_channel", label: "Canal de contacto", options: "preferred_channel", required: true, showWhen: d => d.purpose !== "simulate_only" },
],
project: [
{ key: "project_title", label: "Título do projecto", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "project_summary", label: "Resumo executivo", type: "textarea", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "project_type", label: "Tipo de projecto", options: "project_type", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "project_type_other", label: "Especifique o tipo de projecto", required: true, showWhen: d => d.purpose !== "simulate_only" && d.project_type === "other" },
{ key: "sector_id", label: "Sector de actividade", options: "sector", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "activity_id", label: "Actividade económica", options: "activity", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "products_services", label: "Produtos e serviços", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
{ key: "maturity_stage", label: "Fase de maturidade", options: "maturity_stage", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "expected_start_date", label: "Data prevista de início", type: "date", showWhen: d => d.purpose !== "simulate_only" },
{ key: "project_duration_years", label: "Duração prevista (anos)", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "existing_project_id", label: "Referência do projecto existente", showWhen: d => d.project_type === "expansion" },
],
location: [
{ key: "province_id", label: "Província", options: "province", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "district_id", label: "Distrito", options: "district", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "admin_post_id", label: "Posto administrativo", options: "admin_post", showWhen: d => d.purpose !== "simulate_only" },
{ key: "locality_id", label: "Localidade", options: "locality", showWhen: d => d.purpose !== "simulate_only" },
{ key: "site_known", label: "Local conhecido?", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "land_status", label: "Situação do terreno", options: "land_status", showWhen: d => d.purpose !== "simulate_only" },
{ key: "area_required", label: "Área necessária", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "area_unit", label: "Unidade da área", options: "area_unit", showWhen: d => d.purpose !== "simulate_only" },
{ key: "latitude", label: "Latitude", type: "number", showWhen: d => d.site_known === "yes" },
{ key: "longitude", label: "Longitude", type: "number", showWhen: d => d.site_known === "yes" },
{ key: "special_zone_id", label: "Zona especial ZEE/ZFI", showWhen: d => d.purpose !== "simulate_only" },
{ key: "infrastructure_needs", label: "Necessidades de infra-estrutura", options: "infrastructure", multiple: true, showWhen: d => d.purpose !== "simulate_only" },
],
financial: [
{ key: "currency", label: "Moeda", options: "currency", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "total_investment", label: "Investimento total", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "own_capital", label: "Capital próprio", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "domestic_debt", label: "Financiamento interno", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "foreign_debt", label: "Financiamento externo", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "grants", label: "Subvenções", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "secured_amount", label: "Montante mobilizado", type: "number", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "sought_amount", label: "Montante procurado", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "annual_revenue", label: "Receitas anuais estimadas", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "annual_operating_costs", label: "Custos operacionais anuais", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "expected_profit", label: "Lucro esperado", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "financing_term_months", label: "Prazo de financiamento (meses)", type: "number", showWhen: d => Number(d.domestic_debt) + Number(d.foreign_debt) > 0 },
{ key: "funds_origin_countries", label: "Países de origem dos fundos", options: "country", multiple: true, showWhen: d => Number(d.foreign_debt) > 0 },
],
impact: [
{ key: "direct_jobs", label: "Empregos directos", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "indirect_jobs", label: "Empregos indirectos", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "national_jobs", label: "Empregos nacionais", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "foreign_jobs", label: "Empregos estrangeiros", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "exports_value", label: "Exportações estimadas", type: "number" },
{ key: "import_substitution_value", label: "Substituição de importações estimada", type: "number" },
{ key: "skills_needed", label: "Qualificações/competências necessárias", type: "textarea" },
{ key: "local_content_percentage", label: "Conteúdo local (%)", type: "number" },
{ key: "environmental_category", label: "Categoria ambiental", options: "environmental_category" },
{ key: "community_benefits", label: "Benefícios comunitários", type: "textarea" },
],
tax: [
{ key: "tax_calculation", label: "Pretende calcular um cenário aritmético?", options: "tax_calculation", required: true },
{ key: "base", label: "Base para o cenário", type: "number", required: true, showWhen: d => d.tax_calculation === "yes" },
{ key: "rate", label: "Taxa hipotética do cenário (%)", type: "number", required: true, showWhen: d => d.tax_calculation === "yes" },
{ key: "reduction", label: "Redução hipotética (%)", type: "number", required: true, showWhen: d => d.tax_calculation === "yes" },
{ key: "sales_taxable", label: "Vendas tributadas", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "sales_exempt", label: "Vendas isentas", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "sales_zero_rate", label: "Vendas à taxa zero", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "exports", label: "Exportações", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "deductible_costs", label: "Custos dedutíveis", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "non_deductible_costs", label: "Custos não dedutíveis", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "carried_losses", label: "Prejuízos transitados", type: "number", showWhen: d => d.purpose !== "simulate_only" },
{ key: "payroll", label: "Dados de remunerações (estrutura prevista)", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
{ key: "outbound_payments", label: "Pagamentos ao exterior (estrutura prevista)", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
{ key: "imports", label: "Importações (estrutura prevista)", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
{ key: "claimed_benefits", label: "Benefícios alegados para validação", type: "textarea", showWhen: d => d.purpose !== "simulate_only" },
{ key: "reference_date", label: "Data de referência", type: "date", required: true, showWhen: d => d.purpose !== "simulate_only" },
],
opportunity: [
{ key: "offer_or_demand", label: "Oferta ou procura", options: "offer_or_demand", showWhen: d => d.manifestation_type === "business" },
{ key: "opportunity_type", label: "Necessidade de negócio", options: "opportunity_type", showWhen: d => d.manifestation_type === "business" },
{ key: "opportunity_type_other", label: "Especifique a necessidade", required: true, showWhen: d => d.manifestation_type === "business" && d.opportunity_type === "other" },
{ key: "opportunity_description", label: "Descrição da necessidade", type: "textarea", showWhen: d => d.manifestation_type === "business" },
{ key: "opportunity_value", label: "Valor da oportunidade", type: "number", showWhen: d => d.manifestation_type === "business" },
{ key: "opportunity_currency", label: "Moeda da oportunidade", options: "currency", showWhen: d => d.manifestation_type === "business" && Boolean(d.opportunity_value) },
{ key: "opportunity_conditions", label: "Condições da oportunidade", type: "textarea", showWhen: d => d.manifestation_type === "business" },
{ key: "opportunity_deadline", label: "Prazo da oportunidade", type: "date", showWhen: d => d.manifestation_type === "business" },
{ key: "confidentiality", label: "Confidencialidade", options: "confidentiality", showWhen: d => d.manifestation_type === "business" },
{ key: "publish_requested", label: "Pretende pedir publicação?", options: "preferred_publication", showWhen: d => d.manifestation_type === "business" },
],
documents: [
{ key: "required_documents", label: "Documentos aplicáveis a preparar", options: "document_types", multiple: true, showWhen: d => d.purpose !== "simulate_only" },
],
review: [
{ key: "terms_accepted", label: "Aceito os termos de utilização", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "truth_declaration", label: "Declaro que as informações são verdadeiras", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "reuse_simulation", label: "Autorizo reutilizar os dados da simulação", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "contact_authorisation", label: "Autorizo contacto para acompanhamento", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "publication_authorisation", label: "Autorizo a publicação solicitada", options: "yes_no", required: true, showWhen: d => d.publish_requested === "yes" },
{ key: "public_fields_confirmation", label: "Confirmo os campos propostos para publicação", options: "yes_no", required: true, showWhen: d => d.publish_requested === "yes" },
{ key: "validation_acknowledged", label: "Confirmo que li os avisos de validação", options: "yes_no", required: true, showWhen: d => d.purpose !== "simulate_only" },
{ key: "final_declaration_name", label: "Nome para a declaração final", required: true, showWhen: d => d.purpose !== "simulate_only" },
],
};
simulationOptions.yes_no = ["yes", "no"];
simulationOptions.infrastructure = ["energia", "água", "estrada", "porto", "ferrovia", "telecomunicações", "armazenamento"];
export function replaceSimulationValue(draft: SimulationDraft, key: string, value: string | string[]): SimulationDraft {
const next = { ...draft, [key]: value };
const dependencies: Record<string, string[]> = {
province_id: ["district_id", "admin_post_id", "locality_id"],
district_id: ["admin_post_id", "locality_id"],
admin_post_id: ["locality_id"],
sector_id: ["activity_id"],
site_known: ["latitude", "longitude"],
project_type: ["project_type_other"],
opportunity_type: ["opportunity_type_other"],
foreign_debt: ["funds_origin_countries"],
tax_calculation: ["base", "rate", "reduction"],
};
const clearChildren = key === "foreign_debt" && Number(value) > 0 && Number(draft[key] ?? 0) > 0 ? [] : (dependencies[key] ?? []);
for (const child of clearChildren) {
delete next[child];
delete next[`${child}_label`];
}
if (key === "purpose" && value === "simulate_only") {
for (const fields of Object.values(simulationFieldGroups)) for (const field of fields) {
if (["purpose", "preferred_language"].includes(field.key)) continue;
delete next[field.key];
delete next[`${field.key}_label`];
}
}
if (key === "manifestation_type" && value !== "business") {
for (const field of simulationFieldGroups.opportunity) { delete next[field.key]; delete next[`${field.key}_label`]; }
}
return next;
}
const privateDraftKeys = new Set([
"person_type", "investor_origin", "tax_residency_country", "entity_stage", "legal_form", "nuit",
"investor_profile_id", "profile_confirmation", "legal_name", "commercial_name", "registration_number",
"representative_name", "representative_capacity", "email", "phone", "address", "contact_country",
"contact_province", "contact_district", "preferred_channel",
]);
export function getAutosaveSnapshot(draft: SimulationDraft): SimulationDraft {
return Object.fromEntries(Object.entries(draft).filter(([key]) => {
const field = key.replace(/_label$/, "");
return !privateDraftKeys.has(field);
}));
}
export function partitionSimulationDraft(draft: SimulationDraft): {
personal_snapshot: Record<string, unknown>;
economic_snapshot: Record<string, unknown>;
} {
const personal_snapshot: Record<string, unknown> = {};
const economic_snapshot: Record<string, unknown> = {};
const personal_labels: Record<string, string | string[]> = {};
const economic_labels: Record<string, string | string[]> = {};
for (const [key, value] of Object.entries(draft)) {
if (key.endsWith("_label")) {
const target = privateDraftKeys.has(key.slice(0, -6)) ? personal_labels : economic_labels;
target[key.slice(0, -6)] = value;
continue;
}
(privateDraftKeys.has(key) ? personal_snapshot : economic_snapshot)[key] = value;
}
if (Object.keys(personal_labels).length) personal_snapshot.selection_labels = personal_labels;
if (Object.keys(economic_labels).length) economic_snapshot.selection_labels = economic_labels;
return { personal_snapshot, economic_snapshot };
}
export function restoreSimulationDraft(economic: Record<string, unknown>, personal: Record<string, unknown>): SimulationDraft {
const draft = { ...economic, ...personal } as Record<string, unknown>;
const labels = { ...(economic.selection_labels as Record<string, unknown> | undefined), ...(personal.selection_labels as Record<string, unknown> | undefined) };
delete draft.selection_labels;
delete draft.personal_snapshot;
if (labels && typeof labels === "object") {
for (const [key, value] of Object.entries(labels)) {
if (typeof value === "string" || (Array.isArray(value) && value.every(item => typeof item === "string"))) {
draft[`${key}_label`] = Array.isArray(draft[key]) ? value : Array.isArray(value) ? value[0] : value;
}
}
}
return draft as SimulationDraft;
}
export function simulationDraftFingerprint(draft: SimulationDraft): string {
const sort = (value: unknown): unknown => {
if (Array.isArray(value)) return value.map(sort);
if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sort(entry)]));
return value;
};
return JSON.stringify(sort(partitionSimulationDraft(draft)));
}
export function validateSimulationDraft(draft: SimulationDraft, sectionId: string): Record<string, string> {
const errors: Record<string, string> = {};
for (const field of simulationFieldGroups[sectionId] ?? []) {
if (field.showWhen && !field.showWhen(draft)) continue;
const value = draft[field.key];
const empty = value === undefined || value === "" || (Array.isArray(value) && value.length === 0);
if (field.required && empty) errors[field.key] = "Este campo é obrigatório.";
if (field.type === "number" && value !== undefined && value !== "") {
const number = Number(value);
if (!Number.isFinite(number) || number < 0) errors[field.key] = "Introduza um número igual ou superior a zero.";
}
if (["rate", "reduction", "local_content_percentage"].includes(field.key) && value !== undefined && value !== "") {
if (Number(value) > 100) errors[field.key] = "O valor não pode exceder 100.";
}
if (field.key === "email" && typeof value === "string" && value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) errors[field.key] = "Introduza um endereço de correio electrónico válido.";
}
if (sectionId === "financial" && ["own_capital", "domestic_debt", "foreign_debt", "grants", "total_investment"].every(key => draft[key] !== undefined && draft[key] !== "")) {
const total = ["own_capital", "domestic_debt", "foreign_debt", "grants"].reduce((sum, key) => sum + Number(draft[key]), 0);
if (Math.abs(total - Number(draft.total_investment)) > 0.01) errors.total_investment = "A soma do capital, dívida e subvenções deve corresponder ao investimento total.";
}
return errors;
}
+5
View File
@@ -0,0 +1,5 @@
export declare function replaceTerritoryPath<T>(
path: readonly T[],
index: number,
selected?: T | null,
): T[];
+3
View File
@@ -0,0 +1,3 @@
export function replaceTerritoryPath(path, index, selected = null) {
return [...path.slice(0, index), ...(selected ? [selected] : [])];
}
@@ -0,0 +1,61 @@
import { useRef, useState } from "react";
import type { FormEvent } from "react";
import { newSubmissionKey } from "@/lib/portal-api";
import { Frame, Field, button, card, input } from "./shared";
type Opportunity = { id: string; slug: string; title: string; sector?: { name: string } | null };
type Props = { record: Opportunity; modalities: string[] };
type Form = {
fullName: string; email: string; phone: string; investorType: string; country: string;
sectorInterest: string; modality: string; investmentRange: string; message: string; privacyConsent: boolean; shareWithProponent: boolean;
};
export default function BusinessInterest({ record, modalities }: Props) {
const [form, setForm] = useState<Form>({ fullName: "", email: "", phone: "", investorType: "empresa", country: "MZ", sectorInterest: record.sector?.name ?? "", modality: modalities[0] ?? "", investmentRange: "", message: "", privacyConsent: false, shareWithProponent: false });
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const [receipt, setReceipt] = useState("");
const key = useRef(newSubmissionKey());
const update = <K extends keyof Form>(field: K, value: Form[K]) => setForm((current) => ({ ...current, [field]: value }));
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!form.privacyConsent) { setError("Aceite o tratamento de dados para enviar o pedido."); return; }
setBusy(true); setError("");
const xsrf = decodeURIComponent(document.cookie.split("; ").find((cookie) => cookie.startsWith("XSRF-TOKEN="))?.slice(11) ?? "");
try {
const response = await fetch("/api/v1/interest-requests", {
method: "POST", credentials: "same-origin",
headers: { Accept: "application/json", "Content-Type": "application/json", "X-XSRF-TOKEN": xsrf, "Idempotency-Key": key.current },
body: JSON.stringify({ ...form, existingBusinessId: record.id }),
});
const data = await response.json();
if (!response.ok) throw new Error(data.message ?? "Não foi possível enviar a manifestação.");
setReceipt(data.data.reference);
key.current = newSubmissionKey();
} catch (cause) {
setError(cause instanceof Error ? cause.message : "Não foi possível enviar a manifestação.");
} finally { setBusy(false); }
};
return (
<Frame title="Manifestar interesse">
<p>Oportunidade: <strong>{record.title}</strong></p>
{receipt ? <section className={card} role="status"><h2 className="text-xl font-bold">Pedido recebido</h2><p>Referência: {receipt}</p><p>A equipa fará a triagem antes de qualquer partilha com o proponente.</p></section> : (
<form className={`${card} space-y-4`} onSubmit={submit}>
<Field label="Nome ou denominação"><input className={input} required minLength={3} maxLength={150} value={form.fullName} onChange={(e) => update("fullName", e.target.value)} /></Field>
<Field label="Email"><input className={input} type="email" required maxLength={150} value={form.email} onChange={(e) => update("email", e.target.value)} /></Field>
<Field label="Telefone"><input className={input} type="tel" maxLength={30} value={form.phone} onChange={(e) => update("phone", e.target.value)} /></Field>
<Field label="Tipo de investidor"><select className={input} value={form.investorType} onChange={(e) => update("investorType", e.target.value)}><option value="empresa">Empresa</option><option value="individual">Pessoa singular</option><option value="fundo">Fundo</option><option value="entidade_publica">Entidade pública</option><option value="outro">Outro</option></select></Field>
<Field label="País"><input className={input} required maxLength={100} value={form.country} onChange={(e) => update("country", e.target.value)} /></Field>
<Field label="Sector de interesse"><input className={input} required maxLength={150} value={form.sectorInterest} onChange={(e) => update("sectorInterest", e.target.value)} /></Field>
<Field label="Modalidade pretendida"><select className={input} required value={form.modality} onChange={(e) => update("modality", e.target.value)}>{modalities.map((modality) => <option key={modality} value={modality}>{modality.replaceAll("_", " ")}</option>)}</select></Field>
<Field label="Intervalo de investimento"><input className={input} maxLength={80} value={form.investmentRange} onChange={(e) => update("investmentRange", e.target.value)} /></Field>
<Field label="Motivação e pedido"><textarea className={input} required minLength={20} maxLength={2000} rows={5} value={form.message} onChange={(e) => update("message", e.target.value)} /></Field>
<label className="flex items-start gap-3"><input type="checkbox" required checked={form.privacyConsent} onChange={(e) => update("privacyConsent", e.target.checked)} /><span>Autorizo o tratamento destes dados para responder a esta manifestação. O envio não confirma elegibilidade nem permite partilha automática com o proponente.</span></label>
<label className="flex items-start gap-3"><input type="checkbox" checked={form.shareWithProponent} onChange={(e) => update("shareWithProponent", e.target.checked)} /><span>Autorizo, se necessário para avaliar esta manifestação, a partilha do meu nome, email, telefone e mensagem com o proponente. Esta autorização é opcional e pode ser retirada junto da equipa.</span></label>
{error && <p role="alert">{error}</p>}
<button className={button} disabled={busy}>{busy ? "A enviar…" : "Enviar manifestação"}</button>
</form>
)}
</Frame>
);
}
@@ -0,0 +1,41 @@
import { Link, router } from "@inertiajs/react";
import { useState } from "react";
import { Frame, button, card, input } from "./shared";
type PublicBusiness = {
id: string; slug: string; title: string; objective?: string | null; stage?: string | null;
amount_min?: string | null; amount_max?: string | null; currency?: string | null;
sector?: { name: string; slug: string } | null; province?: string | null;
};
type Props = { results: { data: PublicBusiness[]; links: { url: string | null; label: string; active: boolean }[] }; filters: { q?: string } };
export default function BusinessMarketplace({ results, filters }: Props) {
const [query, setQuery] = useState(filters.q ?? "");
return (
<Frame title="Oportunidades de negócio">
<p>Explore oportunidades aprovadas para publicação. A informação está sujeita a diligência independente.</p>
<form className="flex flex-wrap gap-3" onSubmit={(event) => {
event.preventDefault();
router.get("/negocios", { q: query || undefined }, { preserveState: true, preserveScroll: true });
}}>
<label className="sr-only" htmlFor="business-search">Pesquisar negócios</label>
<input id="business-search" className={input} value={query} maxLength={120} onChange={(event) => setQuery(event.target.value)} />
<button className={button}>Pesquisar</button>
</form>
{!results.data.length && <p role="status">Não existem oportunidades publicadas com estes critérios.</p>}
<ul className="grid gap-4 md:grid-cols-2">
{results.data.map((record) => (
<li className={card} key={record.id}>
<h2 className="text-xl font-bold"><Link className="underline" href={`/negocios/${record.slug}`}>{record.title}</Link></h2>
<p>{[record.sector?.name, record.province, record.stage].filter(Boolean).join(" · ")}</p>
{(record.amount_min || record.amount_max) && <p>{record.amount_min ?? "—"} – {record.amount_max ?? "—"} {record.currency}</p>}
{record.objective && <p className="mt-3 line-clamp-3">{record.objective}</p>}
</li>
))}
</ul>
<nav aria-label="Paginação das oportunidades" className="flex flex-wrap gap-3">
{results.links.map((link) => link.url ? <Link key={link.label} href={link.url} className={link.active ? "font-bold underline" : "underline"} dangerouslySetInnerHTML={{ __html: link.label }} /> : null)}
</nav>
</Frame>
);
}
@@ -0,0 +1,25 @@
import { Link } from "@inertiajs/react";
import { Frame, button, card } from "./shared";
type Business = {
slug: string; title: string; objective?: string | null; stage?: string | null; activity_code?: string | null;
amount_min?: string | null; amount_max?: string | null; available_percentage?: string | null; currency?: string | null;
province?: string | null; district?: string | null; locality?: string | null; sector?: { name: string; slug: string } | null;
};
export default function BusinessOpportunity({ record }: { record: Business }) {
return (
<Frame title={record.title}>
<Link href="/negocios" className="underline">Voltar às oportunidades</Link>
<section className={`${card} space-y-4`}>
<p>{[record.sector?.name, record.stage, record.activity_code].filter(Boolean).join(" · ")}</p>
{(record.province || record.district || record.locality) && <p>{[record.locality, record.district, record.province].filter(Boolean).join(", ")}</p>}
{(record.amount_min || record.amount_max) && <p>Montante indicativo: {record.amount_min ?? "—"} – {record.amount_max ?? "—"} {record.currency}</p>}
{record.available_percentage && <p>Participação indicada: {record.available_percentage}%</p>}
{record.objective && <div><h2 className="font-bold">Objectivo</h2><p className="whitespace-pre-wrap">{record.objective}</p></div>}
<p className="text-sm">A publicação não constitui garantia de titularidade, avaliação, transmissibilidade ou retorno.</p>
<Link className={button} href={`/negocios/${record.slug}/manifestar-interesse`}>Manifestar interesse</Link>
</section>
</Frame>
);
}
@@ -6,6 +6,21 @@ export type Business = {
classification: string;
version_no: number;
sector_id?: number | null;
public_title?: string | null;
activity_code?: string | null;
stage?: string | null;
objective?: string | null;
currency?: string | null;
amount_min?: string | null;
amount_max?: string | null;
available_percentage?: string | null;
funds_use?: string | null;
submitted_at?: string | null;
published_at?: string | null;
expires_at?: string | null;
next_action?: string | null;
next_action_due_at?: string | null;
assigned_to?: number | null;
payload: {
[key: string]: string | undefined;
title: string;
@@ -21,6 +36,17 @@ export const businessStates: Record<string, string> = {
in_review: "Em revisão",
needs_information: "Pendente de informação",
withdrawn: "Retirado",
triage: "Triagem",
information_pending: "Informação pendente",
review: "Em revisão",
approval_l1: "Aprovação nível 1",
approval_l2: "Aprovação nível 2",
approved: "Aprovado",
published: "Publicado",
suspended: "Suspenso",
rejected: "Rejeitado",
cancelled: "Cancelado",
archived: "Arquivado",
};
export const businessModalities: Record<string, string> = {
company_sale: "Venda da empresa",
@@ -52,6 +52,7 @@ export default function BusinessWizard({
});
const [modality, setModality] = useState(record.modality);
const [sectorId, setSectorId] = useState(record.sector_id ?? null);
const [publicTitle, setPublicTitle] = useState(record.public_title ?? "");
const [version, setVersion] = useState(record.version_no);
const versionRef = useRef(record.version_no);
const saved = useRef(
@@ -59,6 +60,7 @@ export default function BusinessWizard({
payload: record.payload,
modality: record.modality,
sectorId: record.sector_id ?? null,
publicTitle: record.public_title ?? "",
}),
);
const inFlight = useRef(false);
@@ -68,7 +70,7 @@ export default function BusinessWizard({
const [confirmation, setConfirmation] = useState("");
const heading = useRef<HTMLHeadingElement>(null);
const step = Math.max(1, Math.min(8, Number(payload.wizard_step ?? 1)));
const serialized = JSON.stringify({ payload, modality, sectorId });
const serialized = JSON.stringify({ payload, modality, sectorId, publicTitle });
const dirty = serialized !== saved.current;
const save = useCallback(async () => {
if (inFlight.current) return false;
@@ -82,6 +84,7 @@ export default function BusinessWizard({
payload,
modality,
sector_id: sectorId,
public_title: publicTitle || null,
classification: record.classification,
reason: "Gravação do formulário privado por etapas",
version_no: versionRef.current,
@@ -97,7 +100,7 @@ export default function BusinessWizard({
inFlight.current = false;
setBusy(false);
}
}, [serialized, payload, modality, sectorId, record.id, record.classification]);
}, [serialized, payload, modality, sectorId, publicTitle, record.id, record.classification]);
useEffect(() => {
if (!dirty || busy || error) return;
@@ -201,6 +204,20 @@ export default function BusinessWizard({
>
<fieldset className="space-y-4">
<legend className="sr-only">{steps[step - 1][english ? 1 : 0]}</legend>
{step === 3 && (
<Field label={t("Título público proposto", "Proposed public title")}>
<input
className={input}
value={publicTitle}
maxLength={180}
onChange={(event) => setPublicTitle(event.target.value)}
aria-describedby="public-title-help"
/>
<span id="public-title-help" className="text-sm">
{t("Será revisto antes de qualquer publicação.", "Reviewed before any publication.")}
</span>
</Field>
)}
{step === 3 && (
<Field label={t("Sector", "Sector")}>
<select
@@ -294,6 +311,10 @@ export default function BusinessWizard({
)}
{step === 8 && (
<dl className="space-y-3 break-words">
<div>
<dt className="font-semibold">{t("Título público proposto", "Proposed public title")}</dt>
<dd>{publicTitle || "—"}</dd>
</div>
{Object.entries(formSchema).map(([key, field]) => (
<div key={key}>
<dt className="font-semibold">
@@ -17,6 +17,10 @@ type Props = {
canEdit: boolean;
owner: boolean;
canReview: boolean;
canTriage: boolean;
canApproveLevelOne: boolean;
canApproveLevelTwo: boolean;
canPublish: boolean;
modalities: string[];
objects: BusinessObject[];
objectKinds: string[];
@@ -34,6 +38,10 @@ function Editor({
canEdit,
owner,
canReview,
canTriage,
canApproveLevelOne,
canApproveLevelTwo,
canPublish,
modalities,
history,
objects,
@@ -235,6 +243,51 @@ function Editor({
Pedir informação
</button>
)}
{canTriage && record.status === "submitted" && (
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("triage")}>
Iniciar triagem
</button>
)}
{canReview && record.status === "triage" && (
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("in_review")}>
Encaminhar para revisão
</button>
)}
{canReview && record.status === "in_review" && (
<>
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("approval_l1")}>
Enviar para aprovação nível 1
</button>
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("rejected")}>
Rejeitar
</button>
</>
)}
{canApproveLevelOne && record.status === "approval_l1" && (
<>
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("approval_l2")}>
Aprovar nível 1 e enviar ao nível 2
</button>
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("rejected")}>
Rejeitar
</button>
</>
)}
{canApproveLevelTwo && record.status === "approval_l2" && (
<>
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("approved")}>
Aprovar nível 2
</button>
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("rejected")}>
Rejeitar
</button>
</>
)}
{canPublish && record.status === "approved" && (
<button className={button} disabled={busy || note.trim().length < 10} onClick={() => void run("published")}>
Publicar
</button>
)}
{owner && record.status !== "withdrawn" && (
<button
className={button}
+1 -1
View File
@@ -311,7 +311,7 @@ export default function Catalogue({
index={index}
parent={territoryPath[index - 1]?.id ?? ""}
selected={territoryPath[index]}
endpoint="/api/v1/investment/territory-options"
endpoint="/api/v1/catalogs/jurisdictions"
onChange={(value) => {
const path = [
...territoryPath.slice(0, index),
+61 -3
View File
@@ -14,6 +14,11 @@ type Interest = {
investment_range: string | null;
message: string;
status: string;
existing_business_id: string | null;
modality: string | null;
assigned_to: number | null;
next_action: string | null;
next_action_due_at: string | null;
consented_at: string | null;
reviewed_at: string | null;
reviewed_by: number | null;
@@ -34,8 +39,13 @@ function dateTime(value: string | null) {
timeZone: "Africa/Maputo",
}).format(new Date(value));
}
export default function InterestDetail({ interest }: { interest: Interest }) {
const form = useForm({ status: interest.status });
type Transition = { from_status: string | null; to_status: string; actor_id: number | null; note: string | null; created_at: string };
type Assignee = { id: number; name: string };
type Consent = { purpose: string; policy_version: string | null; status: string; granted_at: string | null; withdrawn_at: string | null };
export default function InterestDetail({ interest, history = [], assignees = [], consents = [] }: { interest: Interest; history?: Transition[]; assignees?: Assignee[]; consents?: Consent[] }) {
const form = useForm({ status: interest.status, note: "", next_action: interest.next_action ?? "", next_action_due_at: interest.next_action_due_at?.slice(0, 10) ?? "", assigned_to: interest.assigned_to?.toString() ?? "" });
const consentForm = useForm({ reason: "" });
const rows: [string, string | number | null][] = [
["Nome", interest.full_name],
["Email", interest.email],
@@ -44,6 +54,8 @@ export default function InterestDetail({ interest }: { interest: Interest }) {
["País", interest.country],
["Sector", interest.sector_interest],
["Oportunidade", interest.opportunity_reference],
["Negócio publicado", interest.existing_business_id],
["Modalidade", interest.modality],
["Faixa de investimento", interest.investment_range],
["Recebido em (Maputo)", dateTime(interest.created_at)],
["Consentimento em (Maputo)", dateTime(interest.consented_at)],
@@ -90,6 +102,27 @@ export default function InterestDetail({ interest }: { interest: Interest }) {
<h2 className="font-semibold">Mensagem</h2>
<p className="mt-2 break-words whitespace-pre-wrap">{interest.message}</p>
</section>
<section className="space-y-3">
<h2 className="font-semibold">Consentimentos e versões</h2>
{consents.map((consent, index) => (
<article key={`${consent.purpose}-${index}`} className="rounded border p-3">
<p>{consent.purpose}: {consent.status}</p>
<p className="text-sm">Política {consent.policy_version ?? "—"} · concedido {dateTime(consent.granted_at)}</p>
{consent.withdrawn_at && <p className="text-sm">Revogado {dateTime(consent.withdrawn_at)}</p>}
</article>
))}
{consents.some((consent) => consent.purpose === "share_contact_with_proponent" && consent.status === "granted") && (
<form className="max-w-sm space-y-3" onSubmit={(event) => {
event.preventDefault();
consentForm.patch(`/management/interests/${interest.id}/consent/proponent`);
}}>
<Field label="Motivo do pedido de revogação" error={consentForm.errors.reason}>
<textarea className={control} minLength={10} maxLength={2000} required rows={3} value={consentForm.data.reason} onChange={(event) => consentForm.setData("reason", event.target.value)} />
</Field>
<button className={button} disabled={consentForm.processing}>Revogar autorização de partilha</button>
</form>
)}
</section>
<form
className="max-w-sm space-y-4"
onSubmit={(e) => {
@@ -103,17 +136,42 @@ export default function InterestDetail({ interest }: { interest: Interest }) {
value={form.data.status}
onChange={(e) => form.setData("status", e.target.value)}
>
{["received", "in_review", "contacted", "closed"].map((key) => (
{["received", "submitted", "in_review", "reviewed", "qualification", "qualified", "contacted", "contact_accepted", "referred", "due_diligence", "negotiation", "pending", "refused", "converted", "cancelled", "closed"].map((key) => (
<option key={key} value={key}>
{states[key]}
</option>
))}
</select>
</Field>
<Field label="Nota de acompanhamento" error={form.errors.note}>
<textarea className={control} rows={3} value={form.data.note} onChange={(e) => form.setData("note", e.target.value)} />
</Field>
<Field label="Próxima acção" error={form.errors.next_action}>
<input className={control} value={form.data.next_action} onChange={(e) => form.setData("next_action", e.target.value)} />
</Field>
<Field label="Prazo da próxima acção" error={form.errors.next_action_due_at}>
<input type="date" className={control} value={form.data.next_action_due_at} onChange={(e) => form.setData("next_action_due_at", e.target.value)} />
</Field>
<Field label="Responsável" error={form.errors.assigned_to}>
<select className={control} value={form.data.assigned_to} onChange={(e) => form.setData("assigned_to", e.target.value)}>
<option value="">Sem responsável</option>
{assignees.map((assignee) => <option key={assignee.id} value={assignee.id}>{assignee.name}</option>)}
</select>
</Field>
<button className={button} disabled={form.processing}>
{form.processing ? "A guardar…" : "Guardar estado"}
</button>
</form>
<section className="space-y-3">
<h2 className="font-semibold">Histórico do fluxo</h2>
{!history.length && <p>Sem alterações de estado registadas.</p>}
{history.map((entry, index) => (
<article key={`${entry.created_at}-${index}`} className="rounded border p-3">
<p>{entry.from_status ? `${states[entry.from_status] ?? entry.from_status} → ` : ""}{states[entry.to_status] ?? entry.to_status} · {dateTime(entry.created_at)}</p>
{entry.note && <p className="mt-1 whitespace-pre-wrap">{entry.note}</p>}
</article>
))}
</section>
</Management>
);
}
+27 -3
View File
@@ -9,6 +9,11 @@ type Interest = {
full_name: string;
email: string;
sector_interest: string;
existing_business_id: string | null;
modality: string | null;
assignee: { id: number; name: string } | null;
next_action: string | null;
next_action_due_at: string | null;
status: string;
};
export default function Interests({
@@ -16,17 +21,18 @@ export default function Interests({
filters,
}: {
requests: Paginated<Interest>;
filters: { q?: string; status?: string };
filters: { q?: string; status?: string; follow_up?: string };
}) {
const [q, setQ] = useState(filters.q ?? '');
const [status, setStatus] = useState(filters.status ?? '');
const [followUp, setFollowUp] = useState(filters.follow_up ?? '');
return (
<Management title="Manifestações de interesse">
<form
className="flex flex-wrap gap-3"
onSubmit={(e) => {
e.preventDefault();
router.get('/management/interests', { q, status });
router.get('/management/interests', { q, status, follow_up: followUp });
}}
>
<input
@@ -43,7 +49,7 @@ export default function Interests({
onChange={(e) => setStatus(e.target.value)}
>
<option value="">Todos os estados</option>
{['received', 'in_review', 'contacted', 'closed'].map(
{Object.keys(states).filter((key) => ['received', 'submitted', 'in_review', 'reviewed', 'qualification', 'qualified', 'contacted', 'contact_accepted', 'referred', 'due_diligence', 'negotiation', 'pending', 'refused', 'converted', 'cancelled', 'closed'].includes(key)).map(
(key) => (
<option key={key} value={key}>
{states[key]}
@@ -51,6 +57,12 @@ export default function Interests({
),
)}
</select>
<select aria-label="Acompanhamento" className={`${control} max-w-xs`} value={followUp} onChange={(e) => setFollowUp(e.target.value)}>
<option value="">Todos os acompanhamentos</option>
<option value="overdue">Acções atrasadas</option>
<option value="upcoming">Acções por vencer</option>
<option value="unassigned">Sem responsável</option>
</select>
<button className={button}>Pesquisar</button>
</form>
<div className="overflow-x-auto">
@@ -62,6 +74,10 @@ export default function Interests({
'Nome',
'Email',
'Sector',
'Modalidade',
'Responsável',
'Próxima acção',
'Prazo',
'Estado',
].map((label) => (
<th key={label} className="border-b p-3">
@@ -88,6 +104,14 @@ export default function Interests({
<td className="border-b p-3">
{item.sector_interest}
</td>
<td className="border-b p-3">
{item.modality ?? (item.existing_business_id ? 'Negócio' : '—')}
</td>
<td className="border-b p-3">{item.assignee?.name ?? '—'}</td>
<td className="border-b p-3">{item.next_action ?? '—'}</td>
<td className={`border-b p-3 ${item.next_action_due_at && new Date(item.next_action_due_at) < new Date() ? 'font-semibold text-red-700' : ''}`}>
{item.next_action_due_at ? `${new Date(item.next_action_due_at) < new Date() ? 'Atrasada · ' : ''}${new Intl.DateTimeFormat('pt-MZ', { dateStyle: 'medium', timeZone: 'Africa/Maputo' }).format(new Date(item.next_action_due_at))}` : '—'}
</td>
<td className="border-b p-3">
{states[item.status] ?? item.status}
</td>
+12
View File
@@ -15,8 +15,20 @@ export const button =
"inline-flex items-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground disabled:opacity-50";
export const states: Record<string, string> = {
received: "Recebido",
submitted: "Submetido",
in_review: "Em análise",
contacted: "Contactado",
reviewed: "Revisto",
qualification: "Em qualificação",
qualified: "Qualificado",
contact_accepted: "Contacto aceite",
referred: "Encaminhado",
due_diligence: "Diligência devida",
negotiation: "Em negociação",
pending: "Pendente",
refused: "Recusado",
converted: "Convertido",
cancelled: "Cancelado",
closed: "Encerrado",
draft: "Rascunho",
published: "Publicado",
+2 -99
View File
@@ -3,6 +3,7 @@ import { useState } from "react";
import { PortalMenu } from "@/layouts/portal-navigation";
import { newSubmissionKey } from "@/lib/portal-api";
import { ProvinceSelect } from "@/components/investment-territory";
import { SimulationForm } from "@/components/simulation-form";
const input = "w-full rounded-lg border border-slate-300 bg-white p-3 text-slate-900";
const button =
@@ -293,105 +294,7 @@ function PartnershipForm() {
}
function Simulator() {
const [base, setBase] = useState("");
const [rate, setRate] = useState("");
const [reduction, setReduction] = useState("0");
const [currency, setCurrency] = useState("MZN");
const valid =
base !== "" &&
rate !== "" &&
reduction !== "" &&
Number.isFinite(Number(base)) &&
Number(base) >= 0 &&
Number(base) <= 1e12 &&
Number(rate) >= 0 &&
Number(rate) <= 100 &&
Number(reduction) >= 0 &&
Number(reduction) <= 100;
const gross = (Number(base) * Number(rate)) / 100;
const net = gross * (1 - Number(reduction) / 100);
const format = (n: number) =>
new Intl.NumberFormat("pt-MZ", { style: "currency", currency }).format(n);
return (
<section className="max-w-2xl space-y-5 rounded-xl border bg-white p-6">
<h2 className="text-xl font-bold">Cenário com parâmetros próprios</h2>
<p>
Introduza uma base tributável e uma taxa que pretende comparar. Não existem taxas
legais ou incentivos automáticos neste cálculo; a redução é uma hipótese introduzida
por si.
</p>
<label className="block">
Moeda
<select
className={input}
value={currency}
onChange={(e) => setCurrency(e.target.value)}
>
{["MZN", "USD", "EUR", "ZAR"].map((c) => (
<option key={c}>{c}</option>
))}
</select>
</label>
<label className="block">
Base tributável
<input
className={input}
type="number"
min="0"
max="1000000000000"
step="0.01"
value={base}
onChange={(e) => setBase(e.target.value)}
/>
</label>
<label className="block">
Taxa a simular (%)
<input
className={input}
type="number"
min="0"
max="100"
step="0.01"
value={rate}
onChange={(e) => setRate(e.target.value)}
/>
</label>
<label className="block">
Redução hipotética do imposto (%)
<input
className={input}
type="number"
min="0"
max="100"
step="0.01"
value={reduction}
onChange={(e) => setReduction(e.target.value)}
/>
</label>
<div role="status" className="rounded bg-emerald-50 p-5">
{valid ? (
<>
<p>
Sem redução: <strong>{format(gross)}</strong>
</p>
<p>Redução simulada: {format(gross - net)}</p>
<p>
Total do cenário: <strong>{format(net)}</strong>
</p>
<p className="mt-3 text-sm">
Base × taxa ÷ 100 × (1 − redução ÷ 100). Não inclui outros impostos,
deduções, escalões ou regras sectoriais.
</p>
</>
) : (
<p>Preencha a base e uma taxa válida entre 0 e 100%.</p>
)}
</div>
<Link href="/apoio" className="underline">
Pedir esclarecimento sobre o enquadramento aplicável
</Link>
</section>
);
return <SimulationForm />;
}
export default function Services(p: Props) {
+29 -1
View File
@@ -2,11 +2,20 @@
use App\Http\Controllers\Api\V1\HomeController;
use App\Http\Controllers\Api\V1\InterestRequestController;
use App\Http\Controllers\Api\V1\LegislationSourceController;
use App\Http\Controllers\Api\V1\OpportunityController;
use App\Http\Controllers\Api\V1\ReferenceCatalogController;
use App\Http\Controllers\Api\V1\SectorController;
use App\Http\Controllers\Api\V1\SimulationController;
use App\Http\Controllers\Api\V1\SimulationFormDraftController;
use App\Http\Middleware\CorrelationId;
use App\Http\Controllers\PublicBusinessOpportunityController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
Route::prefix('v1')->middleware(['throttle:api', CorrelationId::class])->group(function () {
Route::get('/catalogs/{catalog}', [ReferenceCatalogController::class, 'index'])
->whereIn('catalog', ['authorities', 'jurisdictions', 'economic-activities']);
Route::get('/home', [HomeController::class, 'show']);
Route::get('/sectors', [SectorController::class, 'index']);
@@ -15,6 +24,25 @@ Route::prefix('v1')->middleware(['throttle:api'])->group(function () {
Route::get('/opportunities/search', [OpportunityController::class, 'search']);
Route::get('/opportunities/{slug}', [OpportunityController::class, 'show']);
Route::get('/business-opportunities', [PublicBusinessOpportunityController::class, 'index']);
Route::get('/business-opportunities/{slug}', [PublicBusinessOpportunityController::class, 'show']);
Route::post('/interest-requests', [InterestRequestController::class, 'store'])
->middleware('throttle:10,1');
Route::get('/legislation/sources', [LegislationSourceController::class, 'index']);
});
Route::prefix('v1')->middleware(['web', 'throttle:api', CorrelationId::class])->group(function () {
Route::get('/questionnaires/{code}', [SimulationController::class, 'questionnaire']);
Route::get('/simulation-drafts/current', [SimulationFormDraftController::class, 'current']);
Route::post('/simulation-drafts', [SimulationFormDraftController::class, 'store'])->middleware('throttle:simulation-draft-writes');
Route::get('/simulation-drafts/{id}', [SimulationFormDraftController::class, 'show']);
Route::put('/simulation-drafts/{id}', [SimulationFormDraftController::class, 'update'])->middleware('throttle:simulation-draft-writes');
Route::delete('/simulation-drafts/{id}', [SimulationFormDraftController::class, 'destroy'])->middleware('throttle:simulation-draft-writes');
Route::post('/simulations', [SimulationController::class, 'store'])->middleware('throttle:simulation-calculations');
Route::get('/simulations/{id}', [SimulationController::class, 'show']);
Route::patch('/simulations/{id}/scenarios/{scenarioId}', [SimulationController::class, 'updateScenario'])->middleware('throttle:simulation-scenario-updates');
Route::get('/simulations/{id}/reports/{reportId}', [SimulationController::class, 'report']);
Route::get('/simulations/{id}/jobs', [SimulationController::class, 'jobs']);
});
+3
View File
@@ -2,7 +2,10 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');
Schedule::command('investmoz:escalate-overdue-tasks')->everyFifteenMinutes()->withoutOverlapping();
+2 -1
View File
@@ -6,6 +6,7 @@ use App\Http\Controllers\InvestmentZoneGeometryController;
use App\Http\Controllers\InvestmentZoneLinkController;
use App\Http\Controllers\InvestmentZoneRelationController;
use App\Http\Middleware\PrivateInvestmentResponse;
use App\Http\Middleware\CorrelationId;
use App\Http\Middleware\RequireZoneMfa;
use Illuminate\Support\Facades\Route;
@@ -14,7 +15,7 @@ Route::get('/api/v2/investment/zone-map', [InvestmentZoneGeometryController::cla
Route::get('/zonas/{id}', [InvestmentZoneController::class, 'show'])->whereUuid('id');
Route::get('/api/v2/investment/zones', [InvestmentZoneController::class, 'index'])->middleware('throttle:api');
Route::get('/api/v2/investment/zones/{id}', [InvestmentZoneController::class, 'show'])->whereUuid('id')->middleware('throttle:api');
Route::middleware(['auth', 'verified', 'can:manage-portal', RequireZoneMfa::class, PrivateInvestmentResponse::class, 'throttle:30,1'])->group(function () {
Route::middleware([CorrelationId::class, 'auth', 'verified', 'can:manage-portal', RequireZoneMfa::class, PrivateInvestmentResponse::class, 'throttle:30,1'])->group(function () {
Route::get('/api/v2/investment/admin/legal-sources', [InvestmentLegalSourceController::class, 'index']);
Route::post('/api/v2/investment/admin/legal-sources', [InvestmentLegalSourceController::class, 'store']);
Route::post('/api/v2/investment/admin/legal-sources/{id}/{action}', [InvestmentLegalSourceController::class, 'transition'])->whereUuid('id')->whereIn('action', ['validate', 'revoke']);
+6
View File
@@ -21,6 +21,7 @@ use App\Http\Controllers\PortalManagementController;
use App\Http\Controllers\PortalServicesController;
use App\Http\Controllers\PublicGeographyController;
use App\Http\Controllers\PublicPortalController;
use App\Http\Controllers\PublicBusinessOpportunityController;
use App\Http\Middleware\AiEnabled;
use App\Http\Middleware\EducationEnabled;
use Illuminate\Support\Facades\Route;
@@ -34,6 +35,7 @@ foreach (['sectores' => 'sectors', 'como-investir' => 'procedures', 'legislacao'
'formularios/investidor' => 'investor', 'formularios/empresa' => 'company'] as $path => $page) {
Route::get('/'.$path, [PortalServicesController::class, 'page'])->defaults('page', $page)->name('services.'.$page);
}
Route::get('/Simulador', [PortalServicesController::class, 'page'])->defaults('page', 'simulator')->name('services.simulator.alias');
Route::get('/documentos/{id}/download', [PortalServicesController::class, 'download'])->whereNumber('id')->name('services.download');
Route::post('/apoio', [PortalServicesController::class, 'submit'])->middleware('throttle:10,1')->name('services.submit');
Route::middleware(['auth', 'verified'])->group(function () {
@@ -50,6 +52,9 @@ Route::get('/carteira', [InvestmentPortfolioController::class, 'index'])->name('
Route::get('/carteira/{id}', [InvestmentPortfolioController::class, 'show'])->whereUuid('id')->name('portfolio.show');
Route::get('/oportunidades', [InvestmentDiscoveryController::class, 'index'])->name('portal.opportunities');
Route::get('/oportunidades/{slug}', [InvestmentDiscoveryController::class, 'detail'])->name('portal.opportunity');
Route::get('/negocios', [PublicBusinessOpportunityController::class, 'index'])->name('business-marketplace.index');
Route::get('/negocios/{slug}/manifestar-interesse', [PublicBusinessOpportunityController::class, 'interest'])->name('business-marketplace.interest');
Route::get('/negocios/{slug}', [PublicBusinessOpportunityController::class, 'show'])->name('business-marketplace.show');
Route::get('/sectores/{slug}', [PublicPortalController::class, 'sector'])->name('portal.sector');
Route::get('/manifestar-interesse/{slug?}', [PublicPortalController::class, 'interest'])->name('portal.interest');
Route::get('/acesso-gestao', fn () => Inertia::render('management-access', [
@@ -133,6 +138,7 @@ Route::middleware(['auth', 'verified', 'can:manage-portal'])->prefix('management
Route::get('interests', [$controller, 'interests'])->name('interests');
Route::get('interests/{interest}', [$controller, 'interest'])->name('interest');
Route::patch('interests/{interest}', [$controller, 'review'])->name('review');
Route::patch('interests/{interest}/consent/proponent', [$controller, 'withdrawProponentSharingConsent'])->name('consent.withdraw-proponent');
Route::get('{resource}', [$controller, 'catalog'])->whereIn('resource', ['sectors', 'opportunities'])->name('catalog');
Route::get('{resource}/create', [$controller, 'edit'])->whereIn('resource', ['sectors', 'opportunities'])->name('create');
Route::get('{resource}/{id}/edit', [$controller, 'edit'])->whereIn('resource', ['sectors', 'opportunities'])->whereNumber('id')->name('edit');
+89
View File
@@ -0,0 +1,89 @@
<?php
namespace Tests\Feature;
use App\Jobs\RunSimulation;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
class ApiContractTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
}
public function test_simulation_jobs_follow_simulation_access_scope_and_contract_shape(): void
{
$token = Str::random(48);
$create = $this->postJson('/api/v1/simulations', ['base' => 0, 'rate' => 0], [
'Idempotency-Key' => (string) Str::uuid(),
'X-Simulation-Token' => $token,
])->assertAccepted();
$simulationId = $create->json('data.id');
$this->getJson("/api/v1/simulations/{$simulationId}/jobs", ['X-Simulation-Token' => $token])
->assertOk()
->assertJsonStructure(['data' => [['id', 'simulation_id', 'job_type', 'status', 'attempts', 'available_at']]])
->assertJsonPath('data.0.job_type', 'execute')
->assertJsonPath('data.0.status', 'pending');
$this->getJson("/api/v1/simulations/{$simulationId}/jobs", ['X-Simulation-Token' => Str::random(48)])
->assertNotFound();
}
public function test_public_legislation_lists_only_validated_current_sources_with_etag_and_correlation(): void
{
$owner = User::factory()->create();
$validator = User::factory()->create();
$validId = (string) Str::uuid();
$today = today()->toDateString();
DB::table('investment_legal_sources')->insert([
['id' => $validId, 'title' => 'Lei de teste', 'url' => 'https://example.org/lei', 'diploma' => 'Diploma A',
'article' => 'Artigo 1', 'competent_entity' => 'Entidade A', 'valid_from' => $today, 'valid_until' => null,
'validation_status' => 'validated', 'owner_id' => $owner->id, 'validator_id' => $validator->id,
'validated_at' => now(), 'version_no' => 3, 'created_at' => now(), 'updated_at' => now()],
['id' => (string) Str::uuid(), 'title' => 'Lei pendente', 'url' => 'https://example.org/pendente', 'diploma' => 'Diploma B',
'article' => 'Artigo 2', 'competent_entity' => 'Entidade B', 'valid_from' => $today, 'valid_until' => null,
'validation_status' => 'pending', 'owner_id' => $owner->id, 'validator_id' => null,
'validated_at' => null, 'version_no' => 1, 'created_at' => now(), 'updated_at' => now()],
['id' => (string) Str::uuid(), 'title' => 'Lei expirada', 'url' => 'https://example.org/expirada', 'diploma' => 'Diploma C',
'article' => 'Artigo 3', 'competent_entity' => 'Entidade C', 'valid_from' => today()->subDay()->toDateString(),
'valid_until' => today()->subDay()->toDateString(), 'validation_status' => 'validated', 'owner_id' => $owner->id,
'validator_id' => $validator->id, 'validated_at' => now(), 'version_no' => 2, 'created_at' => now(), 'updated_at' => now()],
]);
$correlationId = (string) Str::uuid();
$response = $this->getJson('/api/v1/legislation/sources?q=Lei&per_page=1', ['X-Correlation-ID' => $correlationId])
->assertOk()
->assertHeader('ETag')
->assertHeader('X-Correlation-ID', $correlationId)
->assertJsonPath('meta.total', 1)
->assertJsonPath('data.0.id', $validId)
->assertJsonPath('data.0.validation_status', 'validated');
$this->assertStringStartsWith('W/"legal-sources-', $response->headers->get('ETag'));
}
public function test_contract_errors_include_correlation_and_required_preconditions(): void
{
$missingKey = $this->postJson('/api/v1/simulations', ['base' => 1, 'rate' => 1])
->assertStatus(428)
->assertJsonStructure(['message'])
->assertHeader('X-Correlation-ID');
$this->assertTrue(Str::isUuid($missingKey->headers->get('X-Correlation-ID')));
$invalidCorrelation = $this->getJson('/api/v1/legislation/sources', ['X-Correlation-ID' => 'invalid'])
->assertUnprocessable()
->assertJsonPath('code', 'invalid_correlation_id')
->assertHeader('X-Correlation-ID');
$this->assertTrue(Str::isUuid($invalidCorrelation->headers->get('X-Correlation-ID')));
}
}
@@ -0,0 +1,94 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class BusinessMarketplacePublicTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
config(['existing-business.enabled' => true]);
}
public function test_public_catalogue_and_detail_exclude_reserved_fields_and_locations(): void
{
$business = $this->publishedBusiness();
$this->getJson('/api/v1/business-opportunities')->assertOk()
->assertJsonPath('data.0.title', 'Public synthetic opportunity')
->assertJsonMissingPath('data.0.owner_user_id')
->assertJsonMissingPath('data.0.payload')
->assertJsonMissingPath('data.0.geom');
$this->getJson('/api/v1/business-opportunities/'.$business['public_slug'])->assertOk()
->assertJsonPath('data.title', 'Public synthetic opportunity')
->assertJsonMissingPath('data.payload')
->assertJsonMissingPath('data.owner_user_id');
}
public function test_interest_is_limited_to_published_business_and_deduplicated_idempotently(): void
{
$business = $this->publishedBusiness();
$payload = [
'existingBusinessId' => $business['id'],
'modality' => 'company_sale',
'fullName' => 'Synthetic investor',
'email' => 'portal-integration-test@gmail.com',
'phone' => '+258 840000000',
'investorType' => 'empresa',
'country' => 'Moçambique',
'sectorInterest' => 'Energia',
'message' => 'Synthetic interest for the published business listing.',
'privacyConsent' => true,
];
$key = (string) Str::uuid();
$first = $this->postJson('/api/v1/interest-requests', $payload, ['Idempotency-Key' => $key])->assertCreated();
$second = $this->postJson('/api/v1/interest-requests', $payload, ['Idempotency-Key' => $key])->assertCreated();
$this->assertSame($first->json(), $second->json());
$this->assertDatabaseHas('interest_requests', [
'existing_business_id' => $business['id'],
'modality' => 'company_sale',
'status' => 'received',
]);
$this->assertDatabaseCount('interest_requests', 1);
$this->assertDatabaseCount('outbox_events', 1);
$this->postJson('/api/v1/interest-requests', [...$payload, 'message' => 'Another valid message for this active interest.'])
->assertStatus(409);
$this->assertDatabaseCount('interest_requests', 1);
}
/** @return array{id:string,public_slug:string} */
private function publishedBusiness(): array
{
$id = (string) Str::uuid();
$slug = 'public-synthetic-'.$id;
$now = now();
DB::table('existing_businesses')->insert([
'id' => $id,
'code' => 'SYN-'.strtoupper(substr(str_replace('-', '', $id), 0, 12)),
'owner_user_id' => User::factory()->create()->id,
'modality' => 'company_sale',
'status' => 'published',
'classification' => 'confidential',
'version_no' => 1,
'payload' => Crypt::encryptString(json_encode(['title' => 'Private synthetic title'], JSON_THROW_ON_ERROR)),
'public_title' => 'Public synthetic opportunity',
'public_slug' => $slug,
'published_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
return ['id' => $id, 'public_slug' => $slug];
}
}
+68 -14
View File
@@ -29,6 +29,16 @@ class ExistingBusinessIntakeTest extends TestCase
return $user;
}
private function designated(string $role): User
{
$user = User::factory()->create();
$user->forceFill(['two_factor_secret' => Crypt::encryptString('SYNTHETIC'), 'two_factor_confirmed_at' => now()])->save();
config(['portal.managers' => [...config('portal.managers', []), $user->email],
'existing-business.'.$role => [...config('existing-business.'.$role, []), strtolower($user->email)]]);
return $user;
}
/** @return array<string,mixed> */
private function draft(): array
{
@@ -41,6 +51,7 @@ class ExistingBusinessIntakeTest extends TestCase
{
$owner = User::factory()->create();
$reviewer = $this->reviewer();
$triager = $this->designated('triagers');
$this->actingAs($owner);
$id = $this->postJson($this->base, $this->draft())->assertCreated()->json('id');
$url = $this->base.'/'.$id;
@@ -50,17 +61,23 @@ class ExistingBusinessIntakeTest extends TestCase
$this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'note' => 'Synthetic review requested', 'review_consent' => true])->assertOk()->assertJsonPath('version_no', 2);
$this->putJson($url, [...$this->draft(), 'version_no' => 2])->assertForbidden();
$this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Attempted self review'])->assertForbidden();
$this->actingAs($triager)->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'triage', 'note' => 'Synthetic triage completed'])->assertOk()->assertJsonPath('version_no', 3);
$this->actingAs($reviewer)->getJson('/api/v1/existing-business/reviews')->assertOk()->assertJsonPath('records.total', 1);
$this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertConflict();
$this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertOk()->assertJsonPath('version_no', 3);
$this->postJson($url.'/transitions', ['version_no' => 3, 'action' => 'needs_information', 'note' => 'Please clarify synthetic source'])->assertOk();
$this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertConflict();
$this->postJson($url.'/transitions', ['version_no' => 3, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertOk()->assertJsonPath('version_no', 4);
$this->postJson($url.'/transitions', ['version_no' => 4, 'action' => 'needs_information', 'note' => 'Please clarify synthetic source'])->assertOk();
$this->actingAs($owner)->getJson($url)->assertOk()->assertJsonPath('history.0.note', 'Please clarify synthetic source');
$this->postJson($url, [...$this->draft(), '_method' => 'PUT', 'version_no' => 4])->assertOk()->assertJsonPath('version_no', 5);
$this->postJson($url.'/transitions', ['version_no' => 5, 'action' => 'withdrawn', 'note' => 'Synthetic withdrawal requested'])->assertOk();
$this->postJson($url, [...$this->draft(), '_method' => 'PUT', 'version_no' => 5])->assertOk()->assertJsonPath('version_no', 6);
$this->postJson($url.'/transitions', ['version_no' => 6, 'action' => 'withdrawn', 'note' => 'Synthetic withdrawal requested'])->assertOk();
$this->actingAs($reviewer)->getJson($url)->assertForbidden();
$this->getJson('/api/v1/existing-business/reviews')->assertJsonPath('records.total', 0);
$this->assertDatabaseCount('existing_business_reviews', 4);
$this->assertDatabaseCount('existing_business_revisions', 6);
$this->assertDatabaseCount('existing_business_reviews', 5);
$this->assertDatabaseCount('existing_business_revisions', 7);
$this->assertDatabaseCount('business_workflow_transitions', 5);
$this->assertDatabaseCount('outbox_events', 1);
$transition = DB::table('business_workflow_transitions')->where('to_status', 'needs_information')->first();
$this->assertSame('in_review', $transition->from_status);
$this->assertStringNotContainsString('Please clarify', $transition->reason);
$this->assertStringNotContainsString('Please clarify', DB::table('existing_business_reviews')->where('action', 'needs_information')->value('note'));
}
@@ -76,11 +93,14 @@ class ExistingBusinessIntakeTest extends TestCase
'valid_until' => today()->addDay()->toDateString()];
$this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'note' => 'Synthetic review requested', 'review_consent' => true])->assertOk();
$this->postJson($url.'/verifications', $finding)->assertForbidden();
$this->actingAs($reviewer)->postJson($url.'/verifications', [...$finding, 'version_no' => 2])->assertConflict();
$this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertOk();
$triager = $this->designated('triagers');
$this->actingAs($triager)->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'triage', 'note' => 'Synthetic triage completed'])->assertOk();
$this->actingAs($reviewer)->postJson($url.'/transitions', ['version_no' => 3, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertOk();
$this->postJson($url.'/verifications', [...$finding, 'version_no' => 3])->assertConflict();
$finding['version_no'] = 4;
$this->postJson($url.'/verifications', [...$finding, 'valid_until' => null])->assertUnprocessable();
$this->postJson($url.'/verifications', [...$finding, 'object_id' => '00000000-0000-4000-8000-000000000001'])->assertNotFound();
$this->postJson($url.'/verifications', $finding)->assertOk()->assertJsonPath('version_no', 4);
$this->postJson($url.'/verifications', $finding)->assertOk()->assertJsonPath('version_no', 5);
$this->postJson($url.'/verifications', $finding)->assertConflict();
$this->getJson($url)->assertOk()->assertJsonPath('verifications.0.effective_outcome', 'confirmed');
$raw = DB::table('existing_business_revisions')->where('business_id', $id)->where('action', 'verification')->value('snapshot');
@@ -88,8 +108,8 @@ class ExistingBusinessIntakeTest extends TestCase
$this->travel(2)->days();
$this->getJson($url)->assertOk()->assertJsonPath('verifications.0.effective_outcome', 'expired');
$this->travelBack();
$this->postJson($url.'/transitions', ['version_no' => 4, 'action' => 'needs_information', 'note' => 'Synthetic clarification requested'])->assertOk();
$this->actingAs($owner)->putJson($url, [...$this->draft(), 'version_no' => 5])->assertOk();
$this->postJson($url.'/transitions', ['version_no' => 5, 'action' => 'needs_information', 'note' => 'Synthetic clarification requested'])->assertOk();
$this->actingAs($owner)->putJson($url, [...$this->draft(), 'version_no' => 6])->assertOk();
$this->getJson($url)->assertJsonPath('verifications.0.effective_outcome', 'stale');
$this->actingAs(User::factory()->create())->getJson($url)->assertForbidden();
}
@@ -114,12 +134,46 @@ class ExistingBusinessIntakeTest extends TestCase
$draft = $this->draft();
$draft['payload']['valid_until'] = today()->subDay()->toDateString();
$id = $this->postJson($this->base, $draft)->assertCreated()->json('id');
foreach (['published', 'approved', 'submitted'] as $action) {
$this->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 1, 'action' => $action, 'note' => 'Synthetic invalid transition', 'review_consent' => true])->assertUnprocessable();
foreach (['published', 'approved'] as $action) {
$this->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 1, 'action' => $action, 'note' => 'Synthetic invalid transition', 'review_consent' => true])->assertForbidden();
}
$this->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'note' => 'Synthetic invalid transition', 'review_consent' => true])->assertUnprocessable();
$this->assertDatabaseCount('existing_business_reviews', 0);
}
public function test_submission_triage_two_level_approval_and_publication_require_distinct_designations(): void
{
$owner = User::factory()->create();
$triager = $this->designated('triagers');
$reviewer = $this->designated('reviewers');
$levelOne = $this->designated('approvers_l1');
$levelTwo = $this->designated('approvers_l2');
$publisher = $this->designated('publishers');
$sectorId = DB::table('sectors')->insertGetId(['name' => 'Synthetic approval sector', 'slug' => 'synthetic-approval-sector', 'is_active' => true]);
$this->actingAs($owner);
$id = $this->postJson($this->base, $this->draft())->assertCreated()->json('id');
$this->putJson($this->base.'/'.$id, [...$this->draft(), 'version_no' => 1, 'sector_id' => $sectorId,
'public_title' => 'Synthetic public listing'])->assertOk();
$url = $this->base.'/'.$id;
$this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'submitted', 'note' => 'Synthetic submission for staged approval', 'review_consent' => true])->assertOk();
$this->assertDatabaseHas('tasks', ['existing_business_id' => $id, 'task_type' => 'triage', 'status' => 'open']);
$this->actingAs($triager)->postJson($url.'/transitions', ['version_no' => 3, 'action' => 'triage', 'note' => 'Synthetic initial triage completed'])->assertOk();
$this->actingAs($reviewer)->postJson($url.'/transitions', ['version_no' => 4, 'action' => 'in_review', 'note' => 'Synthetic independent review started'])->assertOk();
$this->postJson($url.'/transitions', ['version_no' => 5, 'action' => 'approval_l1', 'note' => 'Synthetic review checks completed'])->assertOk();
$this->actingAs($levelOne)->postJson($url.'/transitions', ['version_no' => 6, 'action' => 'approval_l2', 'note' => 'Synthetic first level approval'])->assertOk();
config(['existing-business.approvers_l2' => [...config('existing-business.approvers_l2'), strtolower($levelOne->email)]]);
$this->actingAs($levelOne)->postJson($url.'/transitions', ['version_no' => 7, 'action' => 'approved', 'note' => 'Synthetic second level approval attempt'])->assertForbidden();
$this->actingAs($levelTwo)->postJson($url.'/transitions', ['version_no' => 7, 'action' => 'approved', 'note' => 'Synthetic independent second level approval'])->assertOk();
$this->actingAs($publisher)->postJson($url.'/transitions', ['version_no' => 8, 'action' => 'published', 'note' => 'Synthetic independent publication decision'])->assertOk()->assertJsonPath('published_at', fn ($value) => is_string($value));
$this->assertDatabaseHas('business_workflow_transitions', ['existing_business_id' => $id, 'from_status' => 'approval_l2', 'to_status' => 'approved']);
$this->assertDatabaseHas('existing_businesses', ['id' => $id, 'status' => 'published']);
$this->assertDatabaseCount('outbox_events', 1);
}
public function test_reserved_objects_are_scoped_versioned_and_never_verified_by_declaration(): void
{
$this->actingAs(User::factory()->create());
+14
View File
@@ -32,6 +32,9 @@ class InterestSubmissionTest extends TestCase
$this->assertDatabaseCount('interest_requests', 1);
$this->assertDatabaseCount('interest_submission_receipts', 1);
$this->assertDatabaseCount('outbox_events', 1);
$this->assertDatabaseCount('consents', 1);
$this->assertDatabaseHas('consents', ['interest_request_id' => InterestRequest::firstOrFail()->id,
'purpose' => 'respond_to_interest', 'status' => 'granted', 'policy_version' => '2026-09-30']);
$this->assertDatabaseCount('jobs', 1);
$event = DB::table('outbox_events')->first();
$this->assertSame('pending', $event->status);
@@ -51,6 +54,17 @@ class InterestSubmissionTest extends TestCase
$this->assertDatabaseCount('outbox_events', 1);
}
public function test_proponent_sharing_is_a_separate_optional_versioned_consent(): void
{
$this->postJson('/api/v1/interest-requests', [...$this->payload(), 'shareWithProponent' => true])->assertCreated();
$interest = InterestRequest::firstOrFail();
$this->assertDatabaseHas('consents', ['interest_request_id' => $interest->id,
'purpose' => 'respond_to_interest', 'status' => 'granted']);
$this->assertDatabaseHas('consents', ['interest_request_id' => $interest->id,
'purpose' => 'share_contact_with_proponent', 'status' => 'granted', 'policy_version' => '2026-09-30']);
$this->assertSame(2, DB::table('consents')->where('interest_request_id', $interest->id)->count());
}
public function test_old_clients_still_work_and_distinct_keys_create_distinct_submissions(): void
{
$this->postJson('/api/v1/interest-requests', $this->payload())->assertCreated();
+76 -1
View File
@@ -8,6 +8,9 @@ use App\Models\Sector;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
@@ -120,9 +123,35 @@ class PortalManagementTest extends TestCase
'email' => 'test@example.invalid', 'investor_type' => 'empresa', 'country' => 'Moçambique',
'sector_interest' => 'Energia', 'message' => 'Pedido de informação para teste.', 'privacy_consent' => true]);
$this->actingAs($manager)->get('/management/interests/'.$interest->id)->assertOk();
$this->patch('/management/interests/'.$interest->id, ['status' => 'in_review', 'reviewed_by' => 999])->assertRedirect();
$this->patch('/management/interests/'.$interest->id, ['status' => 'in_review', 'reviewed_by' => 999,
'next_action' => 'Contactar o investidor', 'next_action_due_at' => now()->addDays(2)->toDateString(), 'assigned_to' => $manager->id,
'note' => 'O pedido foi recebido e encaminhado para análise interna.', 'next_action' => 'Contactar o investidor',
'next_action_due_at' => now()->addDays(2)->toDateString(), 'assigned_to' => $manager->id])->assertRedirect();
$this->assertSame($manager->id, $interest->fresh()->reviewed_by);
$this->assertNotNull($interest->fresh()->reviewed_at);
$transition = DB::table('business_workflow_transitions')->where('interest_request_id', $interest->id)->first();
$this->assertSame('received', $transition->from_status);
$this->assertSame('in_review', $transition->to_status);
$this->assertSame($manager->id, $interest->fresh()->assigned_to);
$this->assertDatabaseHas('tasks', ['interest_request_id' => $interest->id, 'assignee_user_id' => $manager->id,
'title' => 'Contactar o investidor', 'status' => 'open']);
$this->patch('/management/interests/'.$interest->id, ['status' => 'in_review', 'next_action' => 'Contactar o investidor',
'next_action_due_at' => now()->addDays(2)->toDateString(), 'assigned_to' => $manager->id])->assertRedirect();
$this->assertSame(1, DB::table('tasks')->where('interest_request_id', $interest->id)->where('status', 'open')->count());
$this->assertDatabaseHas('tasks', ['interest_request_id' => $interest->id, 'assignee_user_id' => $manager->id,
'title' => 'Contactar o investidor', 'status' => 'open']);
$this->patch('/management/interests/'.$interest->id, ['status' => 'in_review', 'next_action' => 'Enviar resumo ao investidor',
'next_action_due_at' => now()->addDays(4)->toDateString(), 'assigned_to' => $manager->id])->assertRedirect();
$this->assertDatabaseHas('tasks', ['interest_request_id' => $interest->id, 'title' => 'Contactar o investidor',
'status' => 'cancelled', 'outcome' => 'superseded']);
$this->assertSame(1, DB::table('tasks')->where('interest_request_id', $interest->id)->where('status', 'open')->count());
$this->patch('/management/interests/'.$interest->id, ['status' => 'in_review'])->assertRedirect();
$this->assertSame('Enviar resumo ao investidor', $interest->fresh()->next_action);
$this->get('/management/interests?follow_up=upcoming')->assertInertia(fn (AssertableInertia $page) => $page
->component('management/interests')->where('requests.data.0.reference', 'IR-TEST')->where('filters.follow_up', 'upcoming'));
$this->assertSame('O pedido foi recebido e encaminhado para análise interna.', Crypt::decryptString($transition->reason));
$this->assertSame($manager->id, $interest->fresh()->assigned_to);
$this->patch('/management/interests/'.$interest->id, ['status' => 'received'])->assertSessionHasErrors('status');
$this->patch('/management/interests/'.$interest->id, ['status' => 'invalid'])->assertSessionHasErrors('status');
}
@@ -144,6 +173,52 @@ class PortalManagementTest extends TestCase
$this->assertDatabaseCount('interest_requests', 0);
}
public function test_proponent_sharing_consent_can_be_revoked_with_audited_outbox_event(): void
{
$manager = $this->manager();
$interest = InterestRequest::create(['reference' => 'IR-CONSENT', 'full_name' => 'Pessoa de teste',
'email' => 'consent@example.invalid', 'investor_type' => 'empresa', 'country' => 'MZ',
'sector_interest' => 'Energia', 'message' => 'Pedido de partilha para teste.', 'privacy_consent' => true]);
$shareConsentId = (string) Str::uuid();
DB::table('consents')->insert(['id' => $shareConsentId, 'source_system' => 'portal-intake',
'external_id' => 'test-share-consent', 'version_no' => 1, 'data_classification' => 'confidential',
'correlation_id' => (string) Str::uuid(), 'subject_ref' => $interest->reference,
'purpose' => 'share_contact_with_proponent', 'status' => 'granted', 'granted_at' => now(),
'interest_request_id' => $interest->id, 'policy_version' => '2026-09-30', 'created_at' => now(), 'updated_at' => now()]);
$this->actingAs($manager)->patch('/management/interests/'.$interest->id.'/consent/proponent', [
'reason' => 'O titular pediu para não partilhar os seus dados com o proponente.',
])->assertRedirect();
$this->assertDatabaseHas('consents', ['id' => $shareConsentId, 'status' => 'withdrawn']);
$this->assertDatabaseHas('business_workflow_transitions', ['interest_request_id' => $interest->id,
'to_status' => 'received']);
$event = DB::table('outbox_events')->where('event_type', 'InterestConsentWithdrawn')->first();
$this->assertNotNull($event);
$this->assertSame($shareConsentId, $event->aggregate_id);
$this->assertStringNotContainsString('consent@example.invalid', $event->payload);
}
public function test_overdue_task_escalation_is_opt_in_and_idempotent(): void
{
$interest = InterestRequest::create(['reference' => 'IR-SLA', 'full_name' => 'Pessoa de teste',
'email' => 'sla@example.invalid', 'investor_type' => 'empresa', 'country' => 'MZ',
'sector_interest' => 'Energia', 'message' => 'Pedido de teste de escalada.', 'privacy_consent' => true]);
DB::table('tasks')->insert(['source_system' => 'portal-interest', 'external_id' => 'sla-test-task',
'version_no' => 1, 'data_classification' => 'confidential', 'correlation_id' => (string) Str::uuid(),
'interest_request_id' => $interest->id, 'task_type' => 'interest_follow_up', 'title' => 'Contactar',
'status' => 'open', 'priority' => 3, 'due_at' => now()->subHour(), 'created_at' => now(), 'updated_at' => now()]);
config(['existing-business.sla_escalation_enabled' => false]);
$this->artisan('investmoz:escalate-overdue-tasks')->assertSuccessful();
$this->assertDatabaseHas('tasks', ['external_id' => 'sla-test-task', 'priority' => 3]);
config(['existing-business.sla_escalation_enabled' => true]);
$this->artisan('investmoz:escalate-overdue-tasks')->assertSuccessful();
$this->artisan('investmoz:escalate-overdue-tasks')->assertSuccessful();
$this->assertDatabaseHas('tasks', ['external_id' => 'sla-test-task', 'priority' => 1, 'outcome' => 'sla_overdue']);
}
public function test_valid_public_submissions_persist_and_return_distinct_references(): void
{
// DNS validation remains enabled; no mail is sent by this endpoint.
+1 -1
View File
@@ -20,7 +20,7 @@ class PortalServicesTest extends TestCase
public function test_public_sections_render_and_desk_requires_login(): void
{
foreach (['sectores', 'como-investir', 'legislacao', 'documentos', 'apoio', 'parcerias', 'simulador-fiscal', 'formularios/investidor', 'formularios/empresa'] as $path) {
foreach (['sectores', 'como-investir', 'legislacao', 'documentos', 'apoio', 'parcerias', 'simulador-fiscal', 'Simulador', 'formularios/investidor', 'formularios/empresa'] as $path) {
$this->get('/'.$path)->assertOk()->assertInertia(fn (Assert $page) => $page->component('portal/services'));
}
$this->get('/balcao-virtual')->assertRedirect('/login');
+163
View File
@@ -0,0 +1,163 @@
<?php
namespace Tests\Feature;
use Database\Seeders\ReferenceCatalogSeeder;
use Database\Seeders\SectorSeeder;
use Database\Seeders\TerritoryReferenceSeeder;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use RuntimeException;
use Tests\TestCase;
class ReferenceCatalogTest extends TestCase
{
use RefreshDatabase;
public function test_reference_seeds_are_repeatable_and_activity_codes_are_stable(): void
{
$this->seedReferences();
$ids = DB::table('economic_activities')->orderBy('code')->pluck('id', 'code')->all();
$this->seedReferences();
$this->assertCount(6, $ids);
$this->assertSame($ids, DB::table('economic_activities')->orderBy('code')->pluck('id', 'code')->all());
$this->assertSame(6, DB::table('economic_activities')->count());
$this->assertSame(3, DB::table('reference_catalog_versions')->count());
$this->assertDatabaseHas('reference_catalog_versions', [
'catalog_code' => 'economic-activities', 'version_no' => 1, 'locale' => 'pt-MZ', 'status' => 'published',
]);
}
public function test_catalog_search_pagination_and_explicit_version(): void
{
$this->seedReferences();
$this->getJson('/api/v1/catalogs/economic-activities?q=energia&per_page=1')
->assertOk()->assertJsonPath('meta.version', 1)->assertJsonPath('meta.locale', 'pt-MZ')
->assertJsonPath('meta.total', 1)->assertJsonPath('data.0.code', 'energia')
->assertJsonPath('data.0.label', DB::table('sectors')->where('slug', 'energia')->value('name'));
$this->getJson('/api/v1/catalogs/economic-activities?version=1&per_page=2&page=2')
->assertOk()->assertJsonPath('meta.current_page', 2)->assertJsonPath('meta.total', 6)
->assertJsonCount(2, 'data')->assertJsonStructure(['links' => ['next', 'previous']]);
$this->getJson('/api/v1/catalogs/economic-activities?version=999')->assertNotFound();
}
public function test_jurisdiction_dropdown_requires_the_immediate_parent_and_filters_children(): void
{
$this->seedReferences();
$country = DB::table('geo_locations')->where('location_type', 'country')->where('source_system', 'cartografia-workbook')->value('id');
$province = DB::table('geo_locations')->where('location_type', 'province')->where('source_system', 'cartografia-workbook')->orderBy('name')->first();
$this->getJson('/api/v1/catalogs/jurisdictions?type=province&parent_id='.$country.'&per_page=100')
->assertOk()->assertJsonPath('meta.version', 3)->assertJsonPath('meta.locale', 'pt-MZ')->assertJsonPath('meta.total', 11);
$district = DB::table('geo_locations')->where('parent_id', $province->id)->where('location_type', 'district')->first();
$this->getJson('/api/v1/catalogs/jurisdictions?type=district&parent_id='.$province->id.'&q='.$district->name)
->assertOk()->assertJsonPath('data.0.parent_id', $province->id)->assertJsonPath('data.0.name', $district->name)->assertJsonPath('meta.total', 1);
$this->getJson('/api/v1/catalogs/jurisdictions?type=district')->assertUnprocessable()->assertJsonValidationErrors('parent_id');
$this->getJson('/api/v1/catalogs/jurisdictions?type=district&parent_id='.$country)->assertUnprocessable()->assertJsonValidationErrors('parent_id');
}
public function test_economic_activity_dropdown_can_be_filtered_by_sector(): void
{
$this->seedReferences();
$sectorId = DB::table('sectors')->where('slug', 'energia')->value('id');
$this->getJson('/api/v1/catalogs/economic-activities?sector_id='.$sectorId)
->assertOk()->assertJsonPath('meta.total', 1)->assertJsonPath('data.0.code', 'energia');
$this->getJson('/api/v1/catalogs/economic-activities?sector_id=999999999')
->assertUnprocessable()->assertJsonValidationErrors('sector_id');
}
public function test_authorities_are_not_fabricated_before_a_source_is_approved(): void
{
$this->seedReferences();
$this->getJson('/api/v1/catalogs/authorities')->assertOk()->assertJsonPath('data', [])
->assertJsonPath('meta.status', 'not_published')->assertJsonPath('meta.version', null);
}
public function test_published_authority_catalog_only_returns_public_organizations(): void
{
$this->seedReferences();
$versionId = DB::table('reference_catalog_versions')->where('catalog_code', 'authorities')->value('id');
DB::table('reference_catalog_versions')->where('id', $versionId)->update(['status' => 'published']);
foreach ([['Public authority', 'public'], ['Internal authority', 'internal']] as [$name, $classification]) {
$organizationId = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $organizationId, 'name' => $name, 'organization_type' => 'public',
'data_classification' => $classification, 'created_at' => now(), 'updated_at' => now()]);
DB::table('authorities')->insert(['id' => (string) Str::uuid(), 'catalog_version_id' => $versionId,
'organization_id' => $organizationId, 'code' => strtolower(str_replace(' ', '-', $name)),
'authority_type' => 'registry', 'label_pt_mz' => $name, 'source_reference' => 'Synthetic approved source',
'version_no' => 1, 'valid_from' => today()->toDateString(), 'status' => 'published',
'created_at' => now(), 'updated_at' => now()]);
}
$this->getJson('/api/v1/catalogs/authorities')->assertOk()->assertJsonPath('meta.version', 1)
->assertJsonPath('meta.total', 1)->assertJsonPath('data.0.name', 'Public authority');
}
public function test_foreign_keys_and_catalog_migration_rollback_guard(): void
{
$this->seedReferences();
$migration = require database_path('migrations/2026_10_01_000100_create_reference_catalogs.php');
try {
$migration->down();
$this->fail('Rollback must preserve seeded economic activity references.');
} catch (RuntimeException $exception) {
$this->assertStringContainsString('Economic activities exist', $exception->getMessage());
}
$activity = DB::table('economic_activities')->first();
try {
DB::table('economic_activities')->insert([
'id' => (string) Str::uuid(), 'catalog_version_id' => $activity->catalog_version_id, 'sector_id' => 999999999,
'code' => 'invalid-fk', 'label_pt_mz' => 'Inválido', 'version_no' => 1, 'valid_from' => '2026-10-01',
'status' => 'published', 'created_at' => now(), 'updated_at' => now(),
]);
$this->fail('The database accepted an unknown sector reference.');
} catch (QueryException $exception) {
$this->assertSame('23503', $exception->errorInfo[0]);
}
}
public function test_published_catalog_values_are_immutable(): void
{
$this->seedReferences();
$activity = DB::table('economic_activities')->where('code', 'energia')->first();
try {
DB::table('economic_activities')->where('id', $activity->id)->update(['label_pt_mz' => 'Rótulo adulterado']);
$this->fail('Published catalog values must be immutable.');
} catch (QueryException $exception) {
$this->assertSame('42501', $exception->errorInfo[0]);
}
}
public function test_empty_catalog_migration_rolls_back_and_reapplies(): void
{
$migration = require database_path('migrations/2026_10_01_000100_create_reference_catalogs.php');
$migration->down();
$this->assertFalse(Schema::hasTable('economic_activities'));
$this->assertFalse(Schema::hasTable('authorities'));
$this->assertFalse(Schema::hasTable('reference_catalog_versions'));
$migration->up();
$this->assertTrue(Schema::hasTable('economic_activities'));
$this->assertTrue(Schema::hasTable('authorities'));
$this->assertTrue(Schema::hasTable('reference_catalog_versions'));
}
private function seedReferences(): void
{
$this->seed(SectorSeeder::class);
$this->seed(TerritoryReferenceSeeder::class);
$this->seed(ReferenceCatalogSeeder::class);
}
}
+255
View File
@@ -0,0 +1,255 @@
<?php
namespace Tests\Feature;
use App\Jobs\RunSimulation;
use App\Models\User;
use App\Services\SimulationExecution;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Tests\TestCase;
class SimulationExecutionTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
}
public function test_anonymous_execution_keeps_only_token_hash_and_reconciles_immutable_sources(): void
{
$token = Str::random(48);
$key = (string) Str::uuid();
$response = $this->postJson('/api/v1/simulations', ['base' => '1000', 'rate' => '10', 'reduction' => '50'], [
'Idempotency-Key' => $key, 'X-Simulation-Token' => $token,
])->assertAccepted()->assertJsonPath('data.status', 'queued')->assertJsonPath('access_token', $token);
$queuedEtag = $response->headers->get('ETag');
$simulationId = $response->json('data.id');
$this->assertDatabaseHas('simulations', ['id' => $simulationId, 'anonymous_token_hash' => hash_hmac('sha256', $token, (string) config('app.key'))]);
$this->assertDatabaseMissing('simulations', ['id' => $simulationId, 'anonymous_token_hash' => $token]);
app(RunSimulation::class, ['simulationId' => $simulationId, 'actorId' => null, 'tokenHash' => hash_hmac('sha256', $token, (string) config('app.key'))])
->handle(app(SimulationExecution::class));
$completedResponse = $this->getJson('/api/v1/simulations/'.$simulationId, ['X-Simulation-Token' => $token])
->assertOk()->assertJsonPath('data.status', 'completed');
$this->assertNotSame($queuedEtag, $completedResponse->headers->get('ETag'));
$simulationView = $completedResponse->json('data');
$this->assertEqualsWithDelta(50.0, (float) $simulationView['result_snapshot']['indicative_total'], 0.0000001);
$this->assertDatabaseHas('simulation_result_sources', ['source_type' => 'user_input']);
$this->assertSame(1, DB::table('simulation_reports')->where('simulation_id', $simulationId)->where('status', 'ready')->count());
$simulation = DB::table('simulations')->where('id', $simulationId)->first();
$result = DB::table('simulation_results')->where('simulation_id', $simulationId)->first();
$source = DB::table('simulation_result_sources')->where('result_id', $result->id)->first();
$resultCanonical = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$result->result_snapshot])->canonical;
$sourceCanonical = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$source->source_snapshot])->canonical;
$this->assertSame($result->result_sha256, hash('sha256', $resultCanonical));
$this->assertSame($source->source_sha256, hash('sha256', $sourceCanonical));
$this->assertNotNull($simulation->questionnaire_version_id);
try {
DB::transaction(fn () => DB::table('simulation_audit_events')->where('simulation_id', $simulationId)->update(['action' => 'audit.tampered']));
$this->fail('Simulation audit events must be append-only.');
} catch (QueryException) {
$this->assertDatabaseMissing('simulation_audit_events', ['simulation_id' => $simulationId, 'action' => 'audit.tampered']);
}
}
public function test_execution_requires_an_explicit_user_supplied_reduction(): void
{
$this->postJson('/api/v1/simulations', ['base' => '500', 'rate' => '5'], [
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => Str::random(48),
])->assertUnprocessable()->assertJsonValidationErrors('reduction');
}
public function test_idempotency_replays_same_payload_and_rejects_key_reuse_with_different_payload(): void
{
$token = Str::random(48);
$key = (string) Str::uuid();
$headers = ['Idempotency-Key' => $key, 'X-Simulation-Token' => $token];
$first = $this->postJson('/api/v1/simulations', ['base' => '500', 'rate' => '5', 'reduction' => '0'], $headers)->assertAccepted();
$id = $first->json('data.id');
$this->postJson('/api/v1/simulations', ['base' => '500', 'rate' => '5', 'reduction' => '0'], $headers)
->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('replayed', true);
$this->postJson('/api/v1/simulations', ['base' => '500', 'rate' => '6', 'reduction' => '0'], $headers)->assertConflict();
$this->assertSame(1, DB::table('simulations')->where('id', $id)->count());
}
public function test_if_match_is_required_and_stale_versions_return_412_then_success_advances_version(): void
{
$token = Str::random(48);
$response = $this->postJson('/api/v1/simulations', ['base' => '1000', 'rate' => '10', 'reduction' => '0'], [
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
])->assertAccepted();
$id = $response->json('data.id');
$scenarioId = $response->json('data.scenarios.0.id');
app(RunSimulation::class, ['simulationId' => $id, 'actorId' => null, 'tokenHash' => hash_hmac('sha256', $token, (string) config('app.key'))])
->handle(app(SimulationExecution::class));
$response = $this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => $token])->assertOk();
$payload = ['label' => 'Cenário revisto', 'base' => '2000', 'rate' => '8', 'reduction' => '25'];
$url = "/api/v1/simulations/{$id}/scenarios/{$scenarioId}";
$headers = ['X-Simulation-Token' => $token];
$this->patchJson($url, $payload, $headers)->assertStatus(428);
$this->patchJson($url, $payload, $headers + ['If-Match' => '"sim-'.$id.'-v999"'])->assertStatus(412);
$etag = $response->headers->get('ETag');
$updated = $this->patchJson($url, $payload, $headers + ['If-Match' => $etag])->assertAccepted();
$updated->assertHeader('ETag', '"sim-'.$id.'-v3"')->assertJsonPath('data.status', 'queued');
$tokenHash = hash_hmac('sha256', $token, (string) config('app.key'));
app(RunSimulation::class, ['simulationId' => $id, 'actorId' => null, 'tokenHash' => $tokenHash])->handle(app(SimulationExecution::class));
$this->assertSame(2, DB::table('simulation_results')->where('simulation_id', $id)->count());
$this->assertSame(2, DB::table('simulation_reports')->where('simulation_id', $id)->count());
$this->patchJson($url, $payload, $headers + ['If-Match' => $etag])->assertStatus(412);
}
public function test_rls_hides_rows_without_context_and_from_an_unrelated_actor(): void
{
$owner = User::factory()->create(['email_verified_at' => now()]);
$other = User::factory()->create(['email_verified_at' => now()]);
$this->actingAs($owner)->postJson('/api/v1/simulations', ['base' => '10', 'rate' => '1', 'reduction' => '0'], ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted();
$id = DB::table('simulations')->where('owner_user_id', $owner->id)->value('id');
$execution = app(SimulationExecution::class);
DB::unprepared('DO $$ BEGIN CREATE ROLE simulation_rls_test NOLOGIN; EXCEPTION WHEN duplicate_object THEN NULL; END $$');
DB::statement('GRANT USAGE ON SCHEMA public TO simulation_rls_test');
DB::statement('GRANT SELECT ON public.simulations, public.memberships TO simulation_rls_test');
try {
DB::statement('SET ROLE simulation_rls_test');
$execution->setContext($other->id, null);
$this->assertSame(0, (int) DB::scalar('select count(*) from public.simulations where id = ?', [$id]));
$execution->setContext($owner->id, null);
$this->assertSame(1, (int) DB::scalar('select count(*) from public.simulations where id = ?', [$id]));
} finally {
DB::statement('RESET ROLE');
DB::statement('DROP OWNED BY simulation_rls_test');
DB::statement('DROP ROLE IF EXISTS simulation_rls_test');
}
$this->actingAs($other)->getJson('/api/v1/simulations/'.$id)->assertNotFound();
}
public function test_terminal_job_failure_is_reconciled_without_exposing_exception_text(): void
{
$owner = User::factory()->create(['email_verified_at' => now()]);
$this->actingAs($owner)->postJson('/api/v1/simulations', ['base' => '10', 'rate' => '1', 'reduction' => '0'], ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted();
$simulation = DB::table('simulations')->where('owner_user_id', $owner->id)->first();
$job = new RunSimulation($simulation->id, $owner->id, null);
$job->failed(new \RuntimeException('sensitive internal detail'));
$this->assertDatabaseHas('simulation_jobs', ['simulation_id' => $simulation->id, 'status' => 'failed', 'error_code' => 'execution_failed']);
$this->assertDatabaseHas('simulations', ['id' => $simulation->id, 'status' => 'failed', 'error_code' => 'execution_failed']);
$this->assertDatabaseMissing('simulation_audit_events', ['simulation_id' => $simulation->id, 'action' => 'sensitive internal detail']);
}
public function test_organization_membership_scopes_simulation_reads(): void
{
$owner = User::factory()->create(['email_verified_at' => now()]);
$member = User::factory()->create(['email_verified_at' => now()]);
$outsider = User::factory()->create(['email_verified_at' => now()]);
$organizationId = (string) Str::uuid();
DB::table('organizations')->insert(['id' => $organizationId, 'name' => 'Test organization', 'organization_type' => 'private',
'data_classification' => 'internal', 'created_at' => now(), 'updated_at' => now()]);
DB::table('memberships')->insert([
['id' => (string) Str::uuid(), 'user_id' => $owner->id, 'organization_id' => $organizationId, 'role' => 'admin', 'created_at' => now(), 'updated_at' => now()],
['id' => (string) Str::uuid(), 'user_id' => $member->id, 'organization_id' => $organizationId, 'role' => 'viewer', 'created_at' => now(), 'updated_at' => now()],
]);
$this->actingAs($owner)->postJson('/api/v1/simulations', ['base' => '100', 'rate' => '2', 'reduction' => '0', 'organization_id' => $organizationId],
['Idempotency-Key' => (string) Str::uuid()])->assertAccepted();
$id = DB::table('simulations')->where('owner_user_id', $owner->id)->value('id');
$this->actingAs($member)->getJson('/api/v1/simulations/'.$id)->assertOk();
$this->actingAs($outsider)->getJson('/api/v1/simulations/'.$id)->assertNotFound();
DB::unprepared('DO $$ BEGIN CREATE ROLE simulation_rls_test NOLOGIN; EXCEPTION WHEN duplicate_object THEN NULL; END $$');
DB::statement('GRANT USAGE ON SCHEMA public TO simulation_rls_test');
DB::statement('GRANT SELECT, UPDATE ON public.simulations TO simulation_rls_test');
DB::statement('GRANT SELECT ON public.memberships TO simulation_rls_test');
try {
DB::statement('SET ROLE simulation_rls_test');
$execution = app(SimulationExecution::class);
$execution->setContext($member->id, null);
$this->assertSame(1, (int) DB::scalar('select count(*) from public.simulations where id = ?', [$id]));
$this->assertSame(0, DB::affectingStatement('update public.simulations set version_no = version_no + 1 where id = ?', [$id]));
} finally {
DB::statement('RESET ROLE');
DB::statement('DROP OWNED BY simulation_rls_test');
DB::statement('DROP ROLE IF EXISTS simulation_rls_test');
}
DB::table('memberships')->where('user_id', $owner->id)->where('organization_id', $organizationId)->delete();
$this->actingAs($owner)->getJson('/api/v1/simulations/'.$id)->assertNotFound();
}
public function test_postgres_rls_uses_anonymous_token_hmac_not_plain_token(): void
{
$token = Str::random(48);
$response = $this->postJson('/api/v1/simulations', ['base' => '40', 'rate' => '10', 'reduction' => '0'], [
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
])->assertAccepted();
$id = $response->json('data.id');
$tokenHash = hash_hmac('sha256', $token, (string) config('app.key'));
DB::unprepared('DO $$ BEGIN CREATE ROLE simulation_rls_test NOLOGIN; EXCEPTION WHEN duplicate_object THEN NULL; END $$');
DB::statement('GRANT USAGE ON SCHEMA public TO simulation_rls_test');
DB::statement('GRANT SELECT ON public.simulations, public.memberships TO simulation_rls_test');
try {
DB::statement('SET ROLE simulation_rls_test');
$execution = app(SimulationExecution::class);
$execution->setContext(null, hash_hmac('sha256', 'wrong-token', (string) config('app.key')));
$this->assertSame(0, (int) DB::scalar('select count(*) from public.simulations where id = ?', [$id]));
$execution->setContext(null, $tokenHash);
$this->assertSame(1, (int) DB::scalar('select count(*) from public.simulations where id = ?', [$id]));
} finally {
DB::statement('RESET ROLE');
DB::statement('DROP OWNED BY simulation_rls_test');
DB::statement('DROP ROLE IF EXISTS simulation_rls_test');
}
}
public function test_retry_budget_is_bounded_and_failed_attempt_is_reconciled(): void
{
$owner = User::factory()->create(['email_verified_at' => now()]);
$this->actingAs($owner)->postJson('/api/v1/simulations', ['base' => '10', 'rate' => '1', 'reduction' => '0'], ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted();
$simulation = DB::table('simulations')->where('owner_user_id', $owner->id)->first();
$execution = app(SimulationExecution::class);
DB::transaction(function () use ($execution, $owner, $simulation): void {
$execution->setContext($owner->id, null);
DB::table('simulation_scenarios')->where('simulation_id', $simulation->id)->delete();
});
$job = new RunSimulation($simulation->id, $owner->id, null);
try {
$job->handle($execution);
$this->fail('A simulation without a scenario must fail and remain retryable.');
} catch (\RuntimeException $exception) {
$this->assertSame('Simulation has no scenario.', $exception->getMessage());
}
$this->assertSame(3, $job->tries);
$this->assertSame([10, 60], $job->backoff());
$this->assertDatabaseHas('simulation_jobs', ['simulation_id' => $simulation->id, 'status' => 'running', 'attempts' => 1]);
$job->failed(new \RuntimeException('do not persist this detail'));
$this->assertDatabaseHas('simulation_jobs', ['simulation_id' => $simulation->id, 'status' => 'failed', 'attempts' => 3]);
}
public function test_empty_schema_rolls_back_and_reapplies_with_questionnaire_seed(): void
{
$migration = require database_path('migrations/2026_10_01_000200_create_simulation_execution_core.php');
$migration->down();
$this->assertFalse(Schema::hasTable('simulations'));
$migration->up();
$this->assertTrue(Schema::hasTable('simulation_results'));
$this->assertDatabaseHas('questionnaire_versions', ['id' => '61000100-0000-4000-8000-000000000001', 'status' => 'published']);
}
public function test_questionnaire_endpoint_exposes_only_published_version_with_etag(): void
{
$this->getJson('/api/v1/questionnaires/user-entered-arithmetic')->assertOk()
->assertHeader('ETag', '"questionnaire-61000100-0000-4000-8000-000000000001-v1"')
->assertJsonPath('data.locale', 'pt-MZ')->assertJsonPath('data.schema_snapshot.rate_origin', 'user_input');
$this->getJson('/api/v1/questionnaires/unknown')->assertNotFound();
}
}
+88
View File
@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class SimulationFormDraftTest extends TestCase
{
use RefreshDatabase;
public function test_anonymous_draft_is_encrypted_scoped_and_restorable(): void
{
$token = Str::random(48);
$key = (string) Str::uuid();
$payload = $this->payload();
$created = $this->postJson('/api/v1/simulation-drafts', $payload, [
'Idempotency-Key' => $key, 'X-Simulation-Token' => $token,
])->assertCreated()->assertJsonPath('data.version_no', 1);
$id = $created->json('data.id');
$stored = DB::table('simulation_form_drafts')->where('id', $id)->first();
$this->assertNotSame(json_encode($payload['personal_snapshot']), $stored->personal_snapshot);
$this->assertNotSame(json_encode($payload['economic_snapshot']), $stored->economic_snapshot);
$this->getJson('/api/v1/simulation-drafts/current', ['X-Simulation-Token' => $token])
->assertOk()->assertJsonPath('data.personal_snapshot.legal_name', 'Entidade de teste')
->assertJsonPath('data.economic_snapshot.base', '1200');
$this->getJson('/api/v1/simulation-drafts/'.$id, ['X-Simulation-Token' => Str::random(48)])->assertNotFound();
}
public function test_creation_is_idempotent_and_rejects_key_reuse_with_changed_data(): void
{
$token = Str::random(48);
$key = (string) Str::uuid();
$headers = ['Idempotency-Key' => $key, 'X-Simulation-Token' => $token];
$payload = $this->payload();
$first = $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertCreated();
$id = $first->json('data.id');
$this->postJson('/api/v1/simulation-drafts', $payload, $headers)
->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('data.replayed', true);
$payload['economic_snapshot']['base'] = '1300';
$this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertConflict();
}
public function test_updates_require_a_current_etag_and_delete_is_reversible_until_expiry(): void
{
$token = Str::random(48);
$headers = ['Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token];
$payload = $this->payload();
$created = $this->postJson('/api/v1/simulation-drafts', $payload, $headers)->assertCreated();
$id = $created->json('data.id');
$url = '/api/v1/simulation-drafts/'.$id;
$this->putJson($url, $payload, ['X-Simulation-Token' => $token])->assertStatus(428);
$this->putJson($url, $payload, ['X-Simulation-Token' => $token, 'If-Match' => '"sfd-'.$id.'-v99"'])->assertStatus(412);
$updated = $this->putJson($url, $payload, ['X-Simulation-Token' => $token, 'If-Match' => $created->headers->get('ETag')])
->assertOk()->assertJsonPath('data.version_no', 2);
$this->deleteJson($url, [], ['X-Simulation-Token' => $token, 'If-Match' => $updated->headers->get('ETag')])->assertNoContent();
$this->assertDatabaseHas('simulation_form_drafts', ['id' => $id]);
$this->assertDatabaseMissing('simulation_form_drafts', ['id' => $id, 'deleted_at' => null]);
}
public function test_server_rejects_invalid_rates_and_unlisted_snapshot_fields(): void
{
$token = Str::random(48);
$payload = $this->payload();
$payload['economic_snapshot']['rate'] = '101';
$this->postJson('/api/v1/simulation-drafts', $payload, [
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot.rate');
$payload = $this->payload();
$payload['economic_snapshot']['unapproved_field'] = 'not accepted';
$this->postJson('/api/v1/simulation-drafts', $payload, [
'Idempotency-Key' => (string) Str::uuid(), 'X-Simulation-Token' => $token,
])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot');
}
/** @return array{personal_snapshot: array<string,string>, economic_snapshot: array<string,string>} */
private function payload(): array
{
return [
'personal_snapshot' => ['legal_name' => 'Entidade de teste', 'email' => 'teste@example.com'],
'economic_snapshot' => ['purpose' => 'simulate_only', 'tax_calculation' => 'yes', 'base' => '1200', 'rate' => '8', 'reduction' => '0'],
];
}
}

Some files were not shown because too many files have changed in this diff Show More