# Esquema Físico da Base de Dados — Versão 1 ## 1. Objectivo Este documento converte o modelo conceptual num esquema físico para Laravel 12 e MySQL 8. Nenhuma migration deve ser criada antes da revisão deste documento. Campos adicionais específicos de cada especialidade serão modelados depois, sem transformar notas clínicas numa colecção descontrolada de colunas. ## 2. Padrões técnicos - Motor: InnoDB. - Charset: `utf8mb4`. - Collation: `utf8mb4_unicode_ci`. - Chave primária: `BIGINT UNSIGNED AUTO_INCREMENT` através de `$table->id()`. - Chaves estrangeiras: `BIGINT UNSIGNED` através de `foreignId()`. - Datas técnicas: `timestamps()` em UTC. - Datas clínicas: `date`; instantes: `dateTime` em UTC. - Dinheiro: `DECIMAL(15,2)` e código ISO da moeda em `CHAR(3)`. - Percentagens: `DECIMAL(5,2)` entre `0.00` e `100.00`. - Estados: `VARCHAR(40)` validado por Enum no Laravel; não usar `ENUM` nativo do MySQL. - Telefones: formato E.164 normalizado, até 20 caracteres. - IP: `VARCHAR(45)` para IPv4 ou IPv6. - UUID público: `CHAR(36)` único quando um identificador não sequencial for exposto. - `softDeletes()` apenas para cadastros recuperáveis; nunca para auditoria, movimentos financeiros ou registos clínicos consolidados. ## 3. Organizações ### `organizations` | Campo | Tipo | Regra | |---|---|---| | id | bigint unsigned | PK | | public_id | char(36) | único | | legal_name | varchar(180) | obrigatório | | trade_name | varchar(180) | obrigatório | | tax_number | varchar(40) | opcional | | email | varchar(180) | opcional | | phone | varchar(20) | opcional | | locale | varchar(10) | padrão `pt_MZ` | | timezone | varchar(64) | padrão `Africa/Maputo` | | currency | char(3) | padrão `MZN` | | status | varchar(40) | `active`, `suspended`, `inactive` | | timestamps | timestamp | obrigatório | Índices: único em `public_id`; índice em `status`; único condicional lógico para `tax_number` preenchido. ### `organization_brandings` `id`, `organization_id`, `logo_path`, `compact_logo_path`, `favicon_path`, `primary_color`, `secondary_color`, `accent_color`, `background_color`, `text_color`, `version` unsigned integer, `is_active` boolean, `published_at`, `timestamps`. Restrição: `(organization_id, version)` único; apenas uma versão activa garantida pela aplicação/transacção. ### `units` `id`, `organization_id`, `public_id`, `code` varchar(30), `name` varchar(150), `email`, `phone`, `address_line_1`, `address_line_2`, `city`, `province`, `country_code` char(2), `timezone`, `status`, `timestamps`, `softDeletes`. Restrições: `(organization_id, code)` e `(organization_id, public_id)` únicos. ### `organization_settings` `id`, `organization_id`, `key` varchar(120), `type` varchar(20), `value_text` text nullable, `value_json` json nullable, `is_sensitive` boolean, `updated_by`, `timestamps`. Restrição: `(organization_id, key)` único. Apenas um dos campos de valor deve ser usado, validado pela aplicação. ### `organization_features` `id`, `organization_id`, `feature_key` varchar(100), `enabled` boolean, `starts_at`, `ends_at`, `configured_by`, `timestamps`. Restrição: `(organization_id, feature_key)` único. ## 4. Utilizadores e autorização ### `users` Manter os campos Laravel e acrescentar: `public_id`, `phone`, `phone_verified_at`, `status`, `must_change_password` boolean, `last_login_at`, `last_login_ip`, `failed_login_attempts` unsigned small integer, `locked_until`, `remember_token`, `timestamps`. `email` pode ser nulo para pacientes sem email. Email e telefone normalizados devem ser únicos quando usados para autenticação. ### `organization_user` `id`, `organization_id`, `user_id`, `default_unit_id` nullable, `status`, `joined_at`, `left_at`, `timestamps`. Restrição: `(organization_id, user_id)` único. ### `roles` `id`, `organization_id` nullable para modelos da plataforma, `name`, `code`, `description`, `is_system`, `timestamps`. Restrição lógica: código único dentro da organização; perfis do sistema não são eliminados. ### `permissions` `id`, `code` varchar(120) único, `name`, `description`, `group`, `timestamps`. ### `permission_role` `role_id`, `permission_id`; PK composta e foreign keys com cascade apenas nesta tabela de associação. ### `role_user` `id`, `organization_id`, `user_id`, `role_id`, `assigned_by`, `assigned_at`, `expires_at` nullable, `timestamps`. Restrição: `(organization_id, user_id, role_id)` único. ### `emergency_access_grants` `id`, `organization_id`, `user_id`, `patient_id`, `reason` text, `granted_at`, `expires_at`, `reviewed_by` nullable, `reviewed_at` nullable, `review_notes` nullable, `timestamps`. Índice: `(organization_id, user_id, expires_at)`. ## 5. Pacientes ### `patients` `id`, `organization_id`, `public_id`, `medical_number` varchar(40), `first_name`, `middle_names` nullable, `last_name`, `display_name`, `birth_date` nullable, `birth_date_estimated` boolean, `clinical_sex` varchar(30) nullable, `marital_status` nullable, `occupation` nullable, `nationality_code` char(2) nullable, `preferred_language`, `status`, `created_by`, `updated_by`, `timestamps`, `softDeletes`. Restrições: `(organization_id, medical_number)` e `(organization_id, public_id)` únicos. Índice para pesquisa em `(organization_id, last_name, first_name)`. ### `patient_identifiers` `id`, `organization_id`, `patient_id`, `type` varchar(40), `number` varchar(100), `issuing_country` char(2), `issued_at`, `expires_at`, `is_primary`, `verified_at`, `timestamps`, `softDeletes`. Restrição: `(organization_id, type, issuing_country, number)` único. ### `patient_contacts` `id`, `organization_id`, `patient_id`, `type`, `value`, `normalized_value`, `is_primary`, `is_verified`, `verified_at`, `timestamps`, `softDeletes`. Índice: `(organization_id, normalized_value)` para detecção de duplicados. ### `patient_addresses` `id`, `organization_id`, `patient_id`, `type`, `address_line_1`, `address_line_2`, `neighbourhood`, `city`, `province`, `postal_code`, `country_code`, `is_primary`, `timestamps`, `softDeletes`. ### `patient_representatives` `id`, `organization_id`, `patient_id`, `representative_patient_id` nullable, `representative_user_id` nullable, `full_name`, `relationship`, `phone`, `email`, `authority_scope` json, `starts_at`, `ends_at`, `verified_by`, `verified_at`, `timestamps`, `softDeletes`. ### `consent_types` `id`, `organization_id`, `code`, `name`, `description`, `current_version`, `required`, `status`, `timestamps`. Restrição: `(organization_id, code)` único. ### `patient_consents` `id`, `organization_id`, `patient_id`, `consent_type_id`, `version`, `decision` varchar(20), `decided_at`, `recorded_by`, `source`, `revoked_at`, `revoked_by`, `revocation_reason`, `timestamps`. Índice: `(organization_id, patient_id, consent_type_id, decided_at)`. ### `allergies` e `chronic_conditions` Ambas incluem `id`, `organization_id`, `patient_id`, nome/código, severidade/estado, observação, `recorded_by`, `recorded_at`, `resolved_at`, `timestamps`. Não usar eliminação física depois de usadas num atendimento. ## 6. Profissionais, especialidades e serviços ### `staff_profiles` `id`, `organization_id`, `user_id`, `employee_number`, `job_title`, `default_unit_id`, `hired_at`, `left_at`, `status`, `timestamps`, `softDeletes`. Restrição: `(organization_id, employee_number)` único. ### `professionals` `id`, `organization_id`, `user_id`, `professional_number`, `issuing_body`, `license_expires_at`, `signature_path`, `status`, `timestamps`, `softDeletes`. Restrição: `(organization_id, professional_number, issuing_body)` único. ### `specialties` `id`, `organization_id`, `code`, `name`, `description`, `status`, `timestamps`, `softDeletes`; `(organization_id, code)` único. ### `professional_specialty` `professional_id`, `specialty_id`, `is_primary`, `starts_at`, `ends_at`; combinação única. ### `services` `id`, `organization_id`, `specialty_id` nullable, `code`, `name`, `description`, `duration_minutes` unsigned small integer, `buffer_minutes` unsigned small integer, `requires_triage`, `requires_prepayment`, `prepayment_percentage`, `allows_partial_payment`, `status`, `timestamps`, `softDeletes`. ### `service_prices` `id`, `organization_id`, `service_id`, `unit_id` nullable, `amount` decimal(15,2), `currency` char(3), `starts_at`, `ends_at`, `created_by`, `timestamps`. Índice: `(organization_id, service_id, unit_id, starts_at)`; períodos sobrepostos são rejeitados na aplicação. ## 7. Agenda e marcações ### `schedules` `id`, `organization_id`, `professional_id`, `unit_id`, `weekday` unsigned tiny integer, `starts_at` time, `ends_at` time, `valid_from`, `valid_until`, `slot_interval_minutes`, `status`, `timestamps`, `softDeletes`. ### `schedule_breaks` `id`, `schedule_id`, `starts_at` time, `ends_at` time, `label`, `timestamps`. ### `unavailabilities` `id`, `organization_id`, `professional_id`, `unit_id` nullable, `starts_at` datetime, `ends_at` datetime, `reason`, `status`, `created_by`, `timestamps`. ### `holidays` `id`, `organization_id`, `unit_id` nullable, `date`, `name`, `is_closed`, `timestamps`; `(organization_id, unit_id, date)` único. ### `appointments` `id`, `organization_id`, `public_id`, `unit_id`, `patient_id`, `professional_id`, `service_id`, `starts_at`, `ends_at`, `status`, `source`, `reason` text nullable, `quoted_amount`, `currency`, `requires_prepayment`, `required_prepayment_amount`, `created_by`, `confirmed_at`, `checked_in_at`, `cancelled_at`, `cancellation_reason`, `timestamps`. Índices: `(organization_id, professional_id, starts_at, ends_at)`, `(organization_id, patient_id, starts_at)`, `(organization_id, unit_id, starts_at, status)` e único em `(organization_id, public_id)`. O bloqueio de conflitos será feito numa transacção com verificação de sobreposição e bloqueio das linhas relevantes; MySQL não possui constraint de exclusão de intervalos. ### `appointment_status_histories` `id`, `organization_id`, `appointment_id`, `from_status`, `to_status`, `reason`, `changed_by`, `changed_at`. Sem `updated_at` e sem eliminação. ### `appointment_links` `id`, `organization_id`, `original_appointment_id`, `new_appointment_id`, `type`, `created_by`, `created_at`; combinação única. ## 8. Processo clínico ### `consultations` `id`, `organization_id`, `public_id`, `appointment_id` único, `patient_id`, `professional_id`, `status`, `started_at`, `completed_at`, `chief_complaint` text nullable, `clinical_summary` longText nullable, `plan` longText nullable, `completed_by` nullable, `timestamps`. ### `triages` `id`, `organization_id`, `consultation_id`, `priority`, `initial_complaint`, `notes`, `recorded_by`, `recorded_at`, `timestamps`. ### `vital_signs` `id`, `organization_id`, `consultation_id`, `type`, `value_decimal` decimal(12,3) nullable, `value_text` nullable, `unit`, `recorded_by`, `recorded_at`, `timestamps`. ### `consultation_notes` `id`, `organization_id`, `consultation_id`, `section`, `content` longText, `version` unsigned integer, `status`, `authored_by`, `authored_at`, `finalized_at`, `timestamps`. ### `diagnoses` `id`, `organization_id`, `consultation_id`, `code_system`, `code`, `description`, `type`, `status`, `diagnosed_by`, `diagnosed_at`, `timestamps`. ### `consultation_addenda` `id`, `organization_id`, `consultation_id`, `reason`, `content` longText, `authored_by`, `authored_at`. Imutável. ### `prescriptions` `id`, `organization_id`, `public_id`, `consultation_id`, `patient_id`, `prescriber_id`, `number`, `status`, `issued_at`, `cancelled_at`, `cancelled_by`, `cancellation_reason`, `timestamps`. Restrição: `(organization_id, number)` único. ### `prescription_items` `id`, `prescription_id`, `medication_name`, `strength`, `dose`, `frequency`, `route`, `duration`, `quantity`, `instructions` text, `sort_order`, `timestamps`. ### `clinical_attachments` `id`, `organization_id`, `patient_id`, `consultation_id` nullable, `uploaded_by`, `category`, `original_name`, `storage_disk`, `storage_path`, `mime_type`, `size_bytes`, `checksum_sha256`, `status`, `created_at`. ## 9. Facturação e pagamentos ### `invoices` `id`, `organization_id`, `public_id`, `unit_id`, `patient_id`, `appointment_id` nullable, `series`, `number`, `status`, `currency`, `subtotal`, `discount_total`, `tax_total`, `total`, `paid_total`, `balance`, `issued_at`, `due_at`, `issued_by`, `cancelled_at`, `cancelled_by`, `cancellation_reason`, `timestamps`. Restrição: `(organization_id, unit_id, series, number)` único. ### `invoice_items` `id`, `invoice_id`, `service_id` nullable, `description`, `quantity` decimal(12,3), `unit_price`, `discount_amount`, `tax_amount`, `line_total`, `sort_order`, `timestamps`. ### `payments` `id`, `organization_id`, `public_id`, `patient_id` nullable, `method`, `provider`, `external_reference`, `currency`, `amount`, `status`, `received_at`, `recorded_by`, `confirmed_at`, `confirmed_by`, `timestamps`. Restrição: `(organization_id, provider, external_reference)` único quando a referência existe. ### `payment_allocations` `id`, `organization_id`, `payment_id`, `invoice_id`, `amount`, `allocated_by`, `allocated_at`, `timestamps`; `(payment_id, invoice_id)` único na Versão 1. ### `payment_transactions` `id`, `organization_id`, `payment_id`, `type`, `amount`, `status`, `provider_reference`, `metadata` json controlado, `processed_by`, `processed_at`, `created_at`. Imutável. ### `refunds` `id`, `organization_id`, `payment_id`, `amount`, `reason`, `status`, `requested_by`, `requested_at`, `approved_by`, `approved_at`, `processed_at`, `transaction_id` nullable, `timestamps`. ### `receipts` `id`, `organization_id`, `public_id`, `unit_id`, `payment_id`, `series`, `number`, `amount`, `currency`, `issued_at`, `issued_by`, `document_id` nullable, `timestamps`; número único por organização/unidade/série. ## 10. Documentos, notificações e auditoria ### `document_templates` `id`, `organization_id`, `type`, `name`, `status`, `timestamps`; `(organization_id, type, name)` único. ### `document_template_versions` `id`, `template_id`, `version`, `content` longText, `allowed_fields` json, `published_at`, `published_by`, `created_at`; `(template_id, version)` único. ### `documents` `id`, `organization_id`, `public_id`, `patient_id` nullable, `type`, `number`, `subject_type`, `subject_id`, `template_version_id`, `storage_disk`, `storage_path`, `checksum_sha256`, `issued_at`, `issued_by`, `published_to_portal_at`, `status`, `timestamps`. ### `notification_templates` `id`, `organization_id`, `event_key`, `channel`, `locale`, `version`, `subject` nullable, `body` text, `status`, `timestamps`; chave única por âmbito/evento/canal/idioma/versão. ### `notifications` `id`, `organization_id`, `patient_id` nullable, `user_id` nullable, `event_key`, `channel`, `destination_masked`, `template_id` nullable, `status`, `scheduled_at`, `sent_at`, `failed_at`, `created_at`. ### `notification_attempts` `id`, `notification_id`, `attempt_number`, `provider`, `provider_reference`, `status`, `error_code`, `error_message` higienizada, `attempted_at`; `(notification_id, attempt_number)` único. ### `audit_logs` `id`, `organization_id` nullable, `actor_user_id` nullable, `action`, `entity_type`, `entity_id`, `old_values` json nullable, `new_values` json nullable, `reason` nullable, `ip_address`, `user_agent` text, `request_id` char(36), `created_at`. Índices: `(organization_id, created_at)`, `(actor_user_id, created_at)`, `(entity_type, entity_id, created_at)`. ## 11. Regras de foreign keys - `restrictOnDelete()` para pacientes, consultas, facturas, pagamentos e documentos. - `nullOnDelete()` apenas para referências auxiliares cuja ausência histórica é aceitável, como utilizador desactivado em certos campos opcionais; a autoria principal deve ser preservada. - `cascadeOnDelete()` apenas em detalhes ainda não consolidados e tabelas pivot, por exemplo itens de um rascunho eliminado antes da emissão. - nenhuma migration deve usar cascade sobre processo clínico ou movimento financeiro consolidado. ## 12. Ordem das migrations 1. organizações, branding, unidades e configurações; 2. extensão de utilizadores e vínculo organizacional; 3. roles, permissions e pivots; 4. pacientes, identificadores, contactos, endereços e representantes; 5. consentimentos, alergias e condições; 6. colaboradores, profissionais e especialidades; 7. serviços e preços; 8. horários, pausas, indisponibilidades e feriados; 9. marcações, histórico e ligações; 10. consultas, triagens, sinais vitais, notas, diagnósticos e adendas; 11. prescrições, itens e anexos; 12. facturas e itens; 13. pagamentos, alocações, movimentos, reembolsos e recibos; 14. modelos, documentos e notificações; 15. auditoria e exportações; 16. índices compostos adicionais e seed dos perfis/permissões. ## 13. Testes obrigatórios do esquema - uma relação não pode cruzar organizações; - números clínicos e financeiros podem repetir apenas em organizações/séries permitidas; - não existe marcação sobreposta sem fluxo de excepção; - uma consulta não muda de paciente relativamente à marcação; - alocações não excedem valores confirmados; - documentos consolidados sobrevivem à inactivação de utilizadores e cadastros; - audit logs não aceitam alteração pela aplicação; - rollback em ambiente de teste respeita a ordem inversa sem foreign keys órfãs. ## 14. Antes de criar as migrations Confirmar com o cliente: identidade fiscal, campos mínimos do paciente, regras para menores, serviços e duração, pré-pagamento, séries documentais, métodos de pagamento e política de retenção. Confirmar tecnicamente a versão real do MySQL do XAMPP, pois algumas constraints dependem dela.