PHPackages                             productiononschool/onsoft-agt - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Payment Processing](/categories/payments)
4. /
5. productiononschool/onsoft-agt

ActiveLibrary[Payment Processing](/categories/payments)

productiononschool/onsoft-agt
=============================

Pacote AGT de Faturação Eletrónica para Angola — Laravel 10/11/12 — compatível com multi-tenant

00PHP

Since Jun 19Pushed todayCompare

[ Source](https://github.com/productionOnschool/onsoft-agt)[ Packagist](https://packagist.org/packages/productiononschool/onsoft-agt)[ RSS](/packages/productiononschool-onsoft-agt/feed)WikiDiscussions main Synced today

READMEChangelog (1)DependenciesVersions (1)Used By (0)

🇦🇴 ONSOFT AGT
=============

[](#-onsoft-agt)

### Pacote Laravel de Faturação Eletrónica para Angola

[](#pacote-laravel-de-faturação-eletrónica-para-angola)

**Compatível com Laravel 10 · 11 · 12 — PHP 8.1+**

[![Packagist](https://camo.githubusercontent.com/ba43d48682795d477425ab2a42aeaffbedd72a84b9844453aae2f71b3d37bf3a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f70726f64756374696f6e6f6e7363686f6f6c2f6f6e736f66742d616774)](https://packagist.org/packages/productiononschool/onsoft-agt)[![License: MIT](https://camo.githubusercontent.com/784362b26e4b3546254f1893e778ba64616e362bd6ac791991d2c9e880a3a64e/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d4d49542d677265656e2e737667)](LICENSE)

**Desenvolvedor:** Adilson Miguel ·  · 2068417074
**Decreto Executivo AGT Angola — 100% Conforme**

---

📦 Instalação
------------

[](#-instalação)

```
composer require productiononschool/onsoft-agt
php artisan onsoft-agt:instalar
```

---

⚙️ Variáveis de Ambiente (.env)
-------------------------------

[](#️-variáveis-de-ambiente-env)

```
# ── Ambiente AGT ──────────────────────────────────────────────────
AGT_AMBIENTE=sandbox                    # sandbox | producao
AGT_MULTI_TENANT=true

# ── Chaves do SOFTWARE (Fabricante Onsoft — partilhadas por todos) ─
# Gerar: openssl genrsa -out priv.pem 2048
#        openssl rsa -in priv.pem -pubout -out pub.pem
# Converter para .env: awk 'NF {printf "%s\\n",$0;}' priv.pem
AGT_SOFTWARE_CHAVE_PRIVADA="-----BEGIN RSA PRIVATE KEY-----\nMIIE...\n-----END RSA PRIVATE KEY-----"
AGT_SOFTWARE_CHAVE_PUBLICA="-----BEGIN PUBLIC KEY-----\nMIIB...\n-----END PUBLIC KEY-----"
AGT_SOFTWARE_NUMERO_CERTIFICACAO=0000
AGT_SOFTWARE_VERSAO_CHAVE=1
AGT_SOFTWARE_NOME="Onsoft AGT"
AGT_SOFTWARE_VERSAO=1.3.0
AGT_SOFTWARE_NIF_FORNECEDOR=500000000

# ── Cada organização configura a sua chave de CONTRIBUINTE no painel ─
# AGT → Configuração → Chaves do Contribuinte
# Guardadas encriptadas com APP_KEY na tabela organization_agt_configs

# ── Configurações gerais ──────────────────────────────────────────
AGT_MOEDA_PADRAO=AOA
AGT_TAXA_IVA_PADRAO=14
```

---

🏗️ Arquitectura de Chaves
-------------------------

[](#️-arquitectura-de-chaves)

```
┌─────────────────────────────────────────────────────────────────────┐
│  CHAVE DO SOFTWARE (Fabricante — Onsoft/Adilson Miguel)             │
│  ─────────────────────────────────────────────────────────────────  │
│  • Representa o FABRICANTE do software de faturação                 │
│  • Registada na AGT UMA VEZ via Declaração Modelo 8                 │
│  • PARTILHADA por todas as organizações (escolas)                   │
│  • Guardada no .env do servidor                                     │
│  • Usada para: jwsSoftwareSignature                                 │
│                                                                     │
│  CHAVE DO CONTRIBUINTE (cada organização/escola)                    │
│  ─────────────────────────────────────────────────────────────────  │
│  • Representa a ESCOLA que emite faturas                            │
│  • Cada escola tem a sua própria chave                              │
│  • Guardada ENCRIPTADA na BD (organization_agt_configs)             │
│  • Desencriptada em memória via Laravel Crypt                       │
│  • Usada para: invoice_hash (RSA-SHA1) + jwsDocumentSignature       │
└─────────────────────────────────────────────────────────────────────┘

```

---

🔄 Sistema de Billing Morph (Extensível)
---------------------------------------

[](#-sistema-de-billing-morph-extensível)

O sistema usa **polimorfismo** no `InvoiceItem` para ligar cada linha de fatura ao seu modelo de origem.

### Como funciona

[](#como-funciona)

```
invoice_items
├── invoiceable_type = "App\Models\Invoice\Billing\BillingPropina"
├── invoiceable_id   = 42
└── → BillingPropina::find(42)

```

### Modelos actuais

[](#modelos-actuais)

ChaveModeloTabelaDescrição`propina``BillingPropina``billing_propinas`Propinas mensais`matricula``BillingMatricula``billing_matriculas`Matrículas`confirmacao``BillingConfirmacao``billing_confirmacoes`Confirmações de matrícula`recurso``BillingRecurso``billing_recursos`Exames de recurso`transporte``BillingTransporte``billing_transportes`Transporte escolar`produto``PedagogicalProduct``pedagogical_products`Produtos pedagógicos### ➕ Adicionar um novo tipo de billing (sem quebrar o pacote)

[](#-adicionar-um-novo-tipo-de-billing-sem-quebrar-o-pacote)

```
// 1. Criar o modelo (igual aos outros)
class BillingSeguro extends Model {
    use BelongsToOrganization;
    protected $table = 'billing_seguros';

    public function invoiceItems(): MorphMany {
        return $this->morphMany(InvoiceItem::class, 'invoiceable');
    }
}

// 2. Registar no AppServiceProvider (UMA LINHA):
\Onsoft\Agt\Suporte\RegistoBillingMorph::registar(
    'seguro',
    \App\Models\Invoice\Billing\BillingSeguro::class
);

// 3. Pronto. PDF, relatórios e snapshots adaptam-se automaticamente.
```

---

📋 Todos os Endpoints
--------------------

[](#-todos-os-endpoints)

### 🔐 Autenticação

[](#-autenticação)

Todos os endpoints requerem autenticação. Proteger com o middleware do projecto (JWT/Sanctum).

---

### 📄 FATURAS

[](#-faturas)

#### `POST /onsoft-agt/faturas`

[](#post-onsoft-agtfaturas)

Criar fatura com múltiplos pagamentos, estudantes e billing morph.

**Request:**

```
{
  "idempotency_key": "uuid-único-por-fatura",
  "document_type": "FR",
  "customer_nif": "500123456",
  "customer_name": "João Silva",
  "customer_email": "joao@gmail.com",
  "encarregadoId": 42,
  "items": [
    {
      "description": "Propina — Outubro 2026",
      "quantity": 1,
      "unit_price": 45000.00,
      "tax_code": "ISE",
      "tax_type": "ISENTO",
      "tax_percentage": 0,
      "tax_reason": "M00",
      "item_category": "propina",
      "product_code": "PROP-OUT-2026",
      "invoiceable_type": "App\\Models\\Invoice\\Billing\\BillingPropina",
      "invoiceable_id": 42,
      "alunoId": 101,
      "aluno_snapshot": { "name": "Maria Silva", "regNumero": "2024/0101" }
    },
    {
      "description": "Transporte — Outubro 2026",
      "quantity": 1,
      "unit_price": 8000.00,
      "tax_code": "IVA",
      "tax_percentage": 14,
      "item_category": "transporte",
      "invoiceable_type": "App\\Models\\Invoice\\Billing\\BillingTransporte",
      "invoiceable_id": 15,
      "alunoId": 101
    }
  ],
  "payments": [
    { "method_code": "NU", "amount": 40000.00 },
    { "method_code": "wallet", "amount": 14120.00 }
  ]
}
```

**Response 201 — Sucesso:**

```
{
  "sucesso": true,
  "mensagem": "Fatura criada com sucesso.",
  "dados": {
    "id": 1247,
    "document_type": "FR",
    "document_no": "FR FR-2026/001247",
    "gross_total": "54120.00",
    "tax_total": "986.67",
    "paid_total": "54120.00",
    "change_amount": "0.00",
    "payment_status": "paid",
    "agt_status": "submitted",
    "invoice_hash": "mYJEv4iGwLcn...",
    "hash_control": "mYJE",
    "jws_document_signature": "eyJhbGci...",
    "jws_software_signature": "eyJhbGci...",
    "items": [ "..." ],
    "payments": [ "..." ]
  }
}
```

**Erros possíveis:**

```
{ "sucesso": false, "mensagem": "FR só pode ser emitida quando totalmente paga. Total: 54120 AOA | Pago: 40000 AOA | Em falta: 14120 AOA." }
{ "sucesso": false, "mensagem": "LICENÇA INACTIVA — A organização não tem licença activa (appCode = false)." }
{ "sucesso": false, "mensagem": "LIMITE DIÁRIO EXCEDIDO — Foram emitidas 50 de 50 faturas permitidas hoje." }
{ "sucesso": false, "mensagem": "Saldo insuficiente na carteira. Saldo: 10000 AOA | Solicitado: 14120 AOA." }
{ "sucesso": false, "mensagem": "Tipo de documento inválido: XX. Suportados: FT, FR, FS, NC, ND, RC" }
```

**Idempotência:**Se enviar o mesmo `idempotency_key` duas vezes, a segunda chamada devolve a fatura já criada sem duplicar. Usar `crypto.randomUUID()` no frontend.

---

#### `POST /onsoft-agt/faturas/pre-visualizar`

[](#post-onsoft-agtfaturaspre-visualizar)

Calcular totais sem criar fatura. Mesmo payload do criar.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "tipo_documento": "FR",
    "subtotal": 53133.33,
    "iva_total": 986.67,
    "gross_total": 54120.00,
    "total_pago": 54120.00,
    "troco": 0.00,
    "em_falta": 0.00,
    "estado_pagamento": "pago"
  }
}
```

---

#### `GET /onsoft-agt/faturas/{id}/pdf`

[](#get-onsoft-agtfaturasidpdf)

PDF da fatura em stream. Usa snapshot se existir (dados imutáveis), dados live como fallback.

**Response:** `Content-Type: application/pdf` (inline no browser)

---

#### `GET /onsoft-agt/faturas/{id}/pdf-snapshot`

[](#get-onsoft-agtfaturasidpdf-snapshot)

PDF exclusivamente do snapshot — re-impressão com dados originais do momento de emissão.

---

#### `GET /onsoft-agt/faturas/{id}/pdf-base64`

[](#get-onsoft-agtfaturasidpdf-base64)

PDF em base64 para o frontend renderizar.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "base64": "JVBERi0xLjQ...",
    "nome_ficheiro": "fr-fr-fr-2026-001247.pdf",
    "mime_type": "application/pdf",
    "tamanho_papel": "A4",
    "copias": 1,
    "mostrar_qr": true
  }
}
```

**Frontend:**

```
const { base64 } = res.data.dados;
window.open(`data:application/pdf;base64,${base64}`);
```

---

#### `POST /onsoft-agt/faturas/{id}/submeter`

[](#post-onsoft-agtfaturasidsubmeter)

Submeter fatura à API AGT.

**Response 200 — AGT activo:**

```
{ "sucesso": true, "mensagem": "Fatura submetida ao AGT com sucesso.", "dados": { "status": "pending", "batch_id": "..." } }
```

**Response 200 — AGT desactivado (simulação):**

```
{ "sucesso": true, "mensagem": "AGT desactivado — submissão simulada localmente.", "dados": { "status": "simulated" } }
```

---

#### `POST /onsoft-agt/faturas/{id}/cancelar`

[](#post-onsoft-agtfaturasidcancelar)

Cancelar fatura.

**Request:** `{ "motivo": "Erro no valor — reemissão necessária" }`

**Response 200 — FR já submetida (gera NC automaticamente):**

```
{
  "sucesso": true,
  "mensagem": "Nota de Crédito emitida automaticamente.",
  "dados": {
    "id": 1248,
    "document_type": "NC",
    "document_no": "NC NC-2026/000012",
    "sourceInvoiceId": 1247
  }
}
```

**Response 200 — FR não submetida:**

```
{ "sucesso": true, "mensagem": "Fatura cancelada localmente.", "dados": { "agt_status": "cancelled" } }
```

**Erros:**

```
{ "sucesso": false, "mensagem": "O motivo de cancelamento é obrigatório." }
{ "sucesso": false, "mensagem": "Esta fatura já está cancelada." }
{ "sucesso": false, "mensagem": "Uma Nota de Crédito não pode ser cancelada." }
```

---

#### `GET /onsoft-agt/faturas/{id}/estado`

[](#get-onsoft-agtfaturasidestado)

Estado da submissão AGT.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "fatura_id": 1247,
    "agt_status": "accepted",
    "submissao": {
      "status": "accepted",
      "attempts": 1,
      "submitted_at": "2026-06-18T14:30:05Z",
      "accepted_at": "2026-06-18T14:30:10Z"
    }
  }
}
```

---

#### `GET /onsoft-agt/faturas/{id}/consult`

[](#get-onsoft-agtfaturasidconsult)

Consultar fatura directamente na API AGT.

---

### 📊 RELATÓRIOS E ESTATÍSTICAS

[](#-relatórios-e-estatísticas)

Todos aceitam filtros via query string:

```
?de=2026-01-01&ate=2026-12-31&document_type=FR&payment_status=paid&excluir_canceladas=1

```

---

#### `GET /onsoft-agt/relatorios/resumo-financeiro`

[](#get-onsoft-agtrelatoriosresumo-financeiro)

Resumo financeiro geral. Para cards do dashboard.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "total_documentos": 1247,
    "total_emitido": 12450000.00,
    "total_pago": 11200000.00,
    "total_divida": 1250000.00,
    "total_iva": 1456780.00,
    "taxa_cobranca": 89.96,
    "por_tipo_documento": [
      { "tipo": "FR", "label": "Fatura-Recibo", "total": 980, "valor": 9800000.00 },
      { "tipo": "FT", "label": "Fatura", "total": 267, "valor": 2650000.00 }
    ]
  }
}
```

---

#### `GET /onsoft-agt/relatorios/receita-por-dia`

[](#get-onsoft-agtrelatoriosreceita-por-dia)

Receita diária. Para gráfico de linha (Chart.js / Recharts).

**Response 200:**

```
{
  "sucesso": true,
  "dados": [
    { "data": "2026-06-01", "faturas": 12, "total_emitido": 540000.00, "total_pago": 540000.00 },
    { "data": "2026-06-02", "faturas": 8,  "total_emitido": 360000.00, "total_pago": 350000.00 }
  ]
}
```

---

#### `GET /onsoft-agt/relatorios/receita-por-mes`

[](#get-onsoft-agtrelatoriosreceita-por-mes)

Receita mensal. Para gráfico de barras.

---

#### `GET /onsoft-agt/relatorios/receita-por-hora`

[](#get-onsoft-agtrelatoriosreceita-por-hora)

Pico de emissão por hora. Para análise operacional.

---

#### `GET /onsoft-agt/relatorios/por-categoria`

[](#get-onsoft-agtrelatoriospor-categoria)

Receita por categoria de billing (propina, matrícula, transporte...).

**Response 200:**

```
{
  "sucesso": true,
  "dados": [
    { "categoria": "propina",    "tipo_label": "BillingPropina",    "faturas": 890, "total": 8900000.00, "iva": 0.00 },
    { "categoria": "transporte", "tipo_label": "BillingTransporte", "faturas": 245, "total": 1960000.00, "iva": 274400.00 }
  ]
}
```

---

#### `GET /onsoft-agt/relatorios/meios-pagamento`

[](#get-onsoft-agtrelatoriosmeios-pagamento)

Distribuição por meio de pagamento. Para gráfico donut/pie.

**Response 200:**

```
{
  "sucesso": true,
  "dados": [
    { "metodo": "NU",     "label": "Numerário",        "faturas": 600, "total": 6000000.00 },
    { "metodo": "WALLET", "label": "Saldo da Carteira", "faturas": 400, "total": 4000000.00 },
    { "metodo": "MX",     "label": "Multicaixa Express","faturas": 247, "total": 2450000.00 }
  ]
}
```

---

#### `GET /onsoft-agt/relatorios/resumo-iva`

[](#get-onsoft-agtrelatoriosresumo-iva)

Resumo de IVA por taxa — para declaração fiscal mensal à AGT.

**Response 200:**

```
{
  "sucesso": true,
  "dados": [
    { "tax_type": "IVA",    "taxa": 14, "taxa_label": "14%",  "faturas": 456, "base_tributavel": 10400000.00, "iva_total": 1456000.00 },
    { "tax_type": "ISENTO", "taxa": 0,  "taxa_label": "Isento","faturas": 791, "base_tributavel": 7900000.00,  "iva_total": 0.00 }
  ]
}
```

---

#### `GET /onsoft-agt/relatorios/estado-agt`

[](#get-onsoft-agtrelatoriosestado-agt)

Estatísticas de submissão à AGT.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "draft":     { "total": 5,    "valor": 25000.00 },
    "pending":   { "total": 12,   "valor": 60000.00 },
    "submitted": { "total": 230,  "valor": 1150000.00 },
    "accepted":  { "total": 1000, "valor": 5000000.00 },
    "rejected":  { "total": 2,    "valor": 10000.00 },
    "failed":    { "total": 3,    "valor": 15000.00 },
    "taxa_submissao": 80.32
  }
}
```

---

#### `GET /onsoft-agt/relatorios/top-clientes?limite=10`

[](#get-onsoft-agtrelatoriostop-clienteslimite10)

Top clientes por valor faturado.

---

#### `GET /onsoft-agt/relatorios/maiores-devedores?limite=10`

[](#get-onsoft-agtrelatoriosmaiores-devedoreslimite10)

Maiores devedores com divida em aberto.

---

#### `GET /onsoft-agt/relatorios/emissoes-30-dias`

[](#get-onsoft-agtrelatoriosemissoes-30-dias)

Emissões nos últimos 30 dias.

---

#### `GET /onsoft-agt/relatorios/limite-diario`

[](#get-onsoft-agtrelatorioslimite-diario)

Estado actual do limite diário de emissão.

**Response 200 — dentro do limite:**

```
{
  "sucesso": true,
  "dados": {
    "licenca_activa": true,
    "limite_activo": true,
    "data_referencia": "2026-06-18",
    "emitidas_hoje": 23,
    "maximo_diario": 50,
    "disponivel_hoje": 27,
    "percentagem_uso": 46.0,
    "bloqueado": false,
    "mensagem_bloqueio": null
  }
}
```

**Response 200 — limite excedido:**

```
{
  "sucesso": true,
  "dados": {
    "licenca_activa": true,
    "limite_activo": true,
    "emitidas_hoje": 50,
    "maximo_diario": 50,
    "disponivel_hoje": 0,
    "percentagem_uso": 100.0,
    "bloqueado": true,
    "mensagem_bloqueio": "Limite diário atingido. Contacte a administração."
  }
}
```

---

#### `GET /onsoft-agt/relatorios/billing-types`

[](#get-onsoft-agtrelatoriosbilling-types)

Lista todos os tipos de billing morph registados.

**Response 200:**

```
{
  "sucesso": true,
  "dados": [
    { "chave": "propina",    "classe": "App\\Models\\Invoice\\Billing\\BillingPropina",    "tabela": "billing_propinas",    "existe": true },
    { "chave": "matricula",  "classe": "App\\Models\\Invoice\\Billing\\BillingMatricula",  "tabela": "billing_matriculas",  "existe": true },
    { "chave": "transporte", "classe": "App\\Models\\Invoice\\Billing\\BillingTransporte", "tabela": "billing_transportes", "existe": true }
  ]
}
```

---

#### `GET /onsoft-agt/relatorios/pdf-listagem?de=2026-01-01&ate=2026-12-31`

[](#get-onsoft-agtrelatoriospdf-listagemde2026-01-01ate2026-12-31)

PDF A4 com listagem completa de faturas. Stream directo para o browser.

#### `GET /onsoft-agt/relatorios/pdf-resumo-financeiro`

[](#get-onsoft-agtrelatoriospdf-resumo-financeiro)

PDF A4 com resumo financeiro completo.

#### `GET /onsoft-agt/relatorios/pdf-iva`

[](#get-onsoft-agtrelatoriospdf-iva)

PDF A4 com relatório de IVA para entrega à AGT.

#### `GET /onsoft-agt/relatorios/pdf-devedores`

[](#get-onsoft-agtrelatoriospdf-devedores)

PDF A4 com lista de devedores.

---

### 🔐 SÉRIES

[](#-séries)

#### `POST /onsoft-agt/series/sincronizar`

[](#post-onsoft-agtseriessincronizar)

Sincronizar séries da API AGT para a BD local.

**Response 200:**

```
{ "sucesso": true, "mensagem": "Sincronizadas 6 séries.", "dados": { "sincronizadas": 6, "erros": [] } }
```

---

### ✅ CONFIGURAÇÃO

[](#-configuração)

#### `GET /onsoft-agt/configuracao/validar`

[](#get-onsoft-agtconfiguracaovalidar)

Validar configuração AGT completa.

**Response 200 — válido:**

```
{ "sucesso": true, "valido": true, "erros": [], "mensagem": "Configuração AGT válida e pronta." }
```

**Response 200 — com erros:**

```
{
  "sucesso": false,
  "valido": false,
  "erros": [
    "[Contribuinte] NIF fiscal não configurado.",
    "[Software] AGT_SOFTWARE_CHAVE_PRIVADA não definida no .env."
  ]
}
```

---

🔒 Imutabilidade Fiscal (AGT Decreto, Anexo I ponto 12l)
-------------------------------------------------------

[](#-imutabilidade-fiscal-agt-decreto-anexo-i-ponto-12l)

Após a criação de uma fatura com hash fiscal, **nenhum campo fiscal pode ser alterado**.

O `InvoiceObserver` bloqueia automaticamente tentativas de alterar:

```
✗ Bloqueado: document_no, document_type, series_code, subtotal,
             gross_total, invoice_hash, jws_*, issued_at,
             organization_snapshot, customer_snapshot

✓ Permitido: agt_status, payment_status, cancel_reason,
             cancelled_at, submission_uuid

```

**Se tentar alterar um campo imutável:**

```
$fatura->gross_total = 99999; // ← lança ExcecaoFaturaAgt
$fatura->save();
// "VIOLAÇÃO DE IMUTABILIDADE FISCAL — A fatura FR FR-2026/001247
//  já foi emitida. Campos fiscais não podem ser alterados: gross_total"
```

**Snapshot imutável criado automaticamente** após cada fatura com hash.

---

🗓️ Reset Ano Fiscal
-------------------

[](#️-reset-ano-fiscal)

Automático: executa 1 Janeiro às 00:01 para todas as organizações activas.

Manual:

```
php artisan onsoft-agt:reset-ano-fiscal 2026 --todas-orgs --fechar-anteriores
php artisan onsoft-agt:reset-ano-fiscal 2026 --organizacaoId=5
```

---

🖨️ Formatos PDF
---------------

[](#️-formatos-pdf)

FormatoPapelQR CodeUsoA4210×297mm✅ 80×80pxFatura standard, relatórios88mmTérmico largo✅ 70×70pxImpressora térmica larga58mmTérmico estreito✅ 55×55pxImpressora térmica estreitaQR Code gerado localmente (`bacon/bacon-qr-code`) — **100% offline, sem internet**.

Conteúdo do QR: `NIF:xxx;DOC:xxx;TIPO:xxx;DATA:xxx;TOTAL:xxx;HASH:xxxx;CERT:xxxx`

Configuração lida automaticamente de `invoice_print_configs`. Sem configuração → A4 por defeito.

---

📜 Tipos de Documento
--------------------

[](#-tipos-de-documento)

CódigoNomeCampos AGTNota`FT`FaturaCreditAmountStandard`FR`Fatura-ReciboCreditAmountDeve estar totalmente pago`FS`Fatura SimplificadaCreditAmountSem NIF obrigatório`NC`Nota de Crédito**DebitAmount**Gerada automaticamente ao cancelar FR submetida`ND`Nota de DébitoCreditAmount`RC`Recibo—Sem assinatura AGT---

💳 Meios de Pagamento
--------------------

[](#-meios-de-pagamento)

CódigoNome`NU`Numerário (Dinheiro)`TB`Transferência Bancária`CC`Cartão de Crédito/Débito`CH`Cheque`MP`Pagamento Móvel`MX`Multicaixa Express`wallet`Saldo da Carteira do Encarregado---

🛠️ Comandos Artisan
-------------------

[](#️-comandos-artisan)

```
php artisan onsoft-agt:instalar                    # Instalação inicial
php artisan onsoft-agt:estado                      # Estado de todas as organizações
php artisan onsoft-agt:sincronizar-series          # Sincronizar séries da AGT
php artisan onsoft-agt:retentar-falhas             # Retentar faturas com falha
php artisan onsoft-agt:reset-ano-fiscal            # Reset séries ano fiscal
php artisan onsoft-agt:verificar-integridade       # Auditoria de integridade fiscal
```

---

✅ Conformidade AGT (Decreto Executivo Angola)
---------------------------------------------

[](#-conformidade-agt-decreto-executivo-angola)

RequisitoStatusHash RSA-SHA1: `InvoiceDate;SystemEntryDate;InvoiceNo;GrossTotal;HashAnterior`✅HashControl: posições 1, 11, 21, 31✅`XxXx-Processado por programa validado nº 0000/AGT`✅RC: `Emitido por programa validado nº 0000/AGT`✅NC usa DebitAmount; FT/FR/FS/ND usam CreditAmount✅Numeração sequencial com lockForUpdate✅Hash chain encadeado por série✅Documento assinado não pode ser alterado✅Séries não podem ser apagadas (só inactivadas)✅Faturas emitidas não podem ser eliminadas✅PDF sem valores negativos✅Consumidor Final quando sem NIF✅QR Code com dados de verificação✅Multi-tenant: chaves encriptadas✅Chave Software no .env, chave Contribuinte na BD✅---

*Onsoft AGT v1.3.0 — Adilson Miguel —  — 2068417074*

---

⚡ Performance e Escalabilidade
------------------------------

[](#-performance-e-escalabilidade)

### Quantas faturas por segundo suporta?

[](#quantas-faturas-por-segundo-suporta)

**Resposta honesta por configuração:**

ConfiguraçãoFaturas/segundoQuando usarMySQL + `CACHE_STORE=database` (actual)~15–20/segDesenvolvimento, escola pequenaMySQL + `CACHE_STORE=redis`~50–80/segProdução, escola médiaMySQL + Redis + Connection Pool~150–200/segRede de escolasMySQL + Redis + 2 servidores PHP~400–600/segGrande rede nacionalRead replicas + Load Balancer~800–1000/segArquitectura enterprise**Contexto real Angola:** Uma escola com 500 alunos no pico de matrículas emite ~50–100 faturas por hora (~0.03/seg). Uma rede de 50 escolas emite ~5.000 faturas por hora (~1.4/seg). O sistema actual aguenta isso **com muito espaço**.

---

### Quantas queries à BD por fatura?

[](#quantas-queries-à-bd-por-fatura)

```
Fatura com 2 itens + 2 pagamentos = ~25 queries

  Verificação idempotency ......... 1 SELECT
  Garantir série fiscal ........... 1 SELECT (+1 INSERT se nova)
  Próximo número (lockForUpdate) .. 1 SELECT + 1 UPDATE
  INSERT da fatura ................ 1 INSERT
  Config AGT (com cache) .......... 1 query (depois usa cache 5 min)
  Fatura anterior (hash chain) .... 1 SELECT
  UPDATE hash + assinaturas ....... 1 UPDATE
  Por cada item (×2):
    INSERT InvoiceItem .............. 1 INSERT
    INSERT InvoiceItemTax ........... 1 INSERT
  Por cada pagamento (×2):
    Wallet (firstOrCreate) .......... 1 SELECT/INSERT
    INSERT InvoicePayment ........... 1 INSERT
    INSERT InvoicePaymentMethod ..... 1 INSERT
    INSERT InvoicePaymentAllocation . 1 INSERT
    UPDATE wallet balance ........... 1 UPDATE
    INSERT WalletMovement ........... 1 INSERT
  Observer snapshot ............... 1 SELECT + 1 INSERT
  ────────────────────────────────────────────
  TOTAL                               ~25 queries

```

---

### Cache da OrganizationAgtConfig

[](#cache-da-organizationagtconfig)

A configuração AGT da organização (chaves, NIF, número de certificação) era carregada **3 vezes separadamente** por fatura. Agora é carregada uma vez e guardada em cache por 5 minutos.

```
// Internamente o pacote faz isto:
$config = cache()->remember("onsoft_agt_config_{$orgId}", 300, fn() =>
    OrganizationAgtConfig::where('organizationId', $orgId)->first()
);
```

**O cache funciona com qualquer driver — não precisa de Redis.**

DriverComo configurarComportamento`database` (padrão)`CACHE_STORE=database`Cache na tabela `cache` da BD`file``CACHE_STORE=file`Cache em `storage/framework/cache/``redis``CACHE_STORE=redis`Cache no Redis (mais rápido)`array``CACHE_STORE=array`Só dura o pedido actual**Se não tiver Redis, o sistema funciona na mesma.** Redis é uma optimização futura.

Quando a config AGT for alterada no painel, o cache é invalidado automaticamente:

```
// Chamar no controller depois de guardar a config:
\Onsoft\Agt\Servicos\ServicoFatura::invalidarCacheConfig($organizacaoId);
```

---

### Índices de BD (obrigatório em produção)

[](#índices-de-bd-obrigatório-em-produção)

O pacote inclui uma migração com **26 índices** nas tabelas críticas. Sem estes índices, queries em tabelas com 100.000+ registos fazem full table scan e demoram segundos. Com índices, as mesmas queries demoram &lt;1ms.

```
php artisan migrate
# Aplica automaticamente: 2024_01_01_000003_performance_indexes.php
```

Tabelas com índices adicionados:

TabelaÍndices adicionados`invoices``(organizationId, issued_at)`, `(organizationId, payment_status)`, `(organizationId, agt_status)`, `(organizationId, idempotency_key)`, + 4 mais`agt_series``(organizationId, document_type, fiscal_year)`, `(organizationId, active)``invoice_items``(invoiceId)`, `(invoiceable_type, invoiceable_id)`, `(itemable_type, itemable_id)``invoice_item_taxes``(invoiceItemId)`, `(organizationId, tax_type)``invoice_payments``(invoiceId)`, `(organizationId, encarregadoId)``invoice_snapshots``(organizationId, invoiceId)``guardian_wallets``(organizationId, encarregadoId)`---

### O `lockForUpdate` na série — o que faz e porque é necessário

[](#o-lockforupdate-na-série--o-que-faz-e-porque-é-necessário)

```
// ServicoSeries::proximoNumeroDocumento()
$bloqueada = AgtSeries::whereKey($serie->id)->lockForUpdate()->firstOrFail();
```

Este lock garante que **duas faturas da mesma organização não recebem o mesmo número** mesmo que sejam criadas em simultâneo (dois utilizadores ao mesmo tempo). Sem este lock, duas faturas poderiam receber `FR-2026/000001`.

**Implicação:** Duas faturas da mesma série não podem ser criadas exactamente em paralelo — a segunda espera a primeira terminar (tipicamente &lt;50ms). Para o contexto escolar, isto nunca é um problema.

---

### Quando activar Redis (passo a passo)

[](#quando-activar-redis-passo-a-passo)

Redis não é obrigatório. Activar quando:

- Tiveres &gt;50 utilizadores simultâneos
- Tiveres múltiplos servidores PHP
- As queries à tabela `cache` da BD começarem a aparecer como bottleneck

**Instalação no servidor Ubuntu/Debian:**

```
sudo apt install redis-server
sudo systemctl enable redis-server
sudo systemctl start redis-server
```

**Alterar no `.env`:**

```
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
```

**Limpar caches antigos:**

```
php artisan cache:clear
php artisan config:clear
php artisan optimize
```

Impacto imediato: **3–4× mais capacidade** sem alterar uma linha de código.

---

### Submissão AGT — síncrona vs assíncrona

[](#submissão-agt--síncrona-vs-assíncrona)

Por defeito, quando `auto_submit_invoices = true`, a submissão à API AGT é **assíncrona via queue**:

```
Utilizador cria fatura
    → fatura criada em BD (~50ms)
    → resposta HTTP 201 devolvida ao utilizador
    → SubmitInvoiceToAgtJob colocado na queue
        → worker processa em background
        → chama API AGT
        → actualiza agt_status

```

O utilizador recebe resposta imediata. A comunicação com a AGT acontece nos segundos seguintes em background. Se a AGT estiver lenta ou em baixo, a fatura fica em `agt_status = pending` e o job retenta automaticamente (3 vezes por defeito).

**Para ver faturas pendentes de submissão:**

```
php artisan onsoft-agt:retentar-falhas --limite=50
```

---

### Checklist de produção

[](#checklist-de-produção)

Antes de lançar em produção, verificar:

```
# 1. Correr migrações (inclui índices de performance)
php artisan migrate

# 2. Verificar configuração AGT
php artisan onsoft-agt:estado
php artisan onsoft-agt:configuracao/validar  # via HTTP

# 3. Optimizar autoloader
composer install --no-dev --optimize-autoloader

# 4. Optimizar Laravel
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

# 5. Verificar queue worker está a correr
php artisan queue:work --queue=agt-faturas --tries=3 --timeout=90

# 6. Configurar supervisor para o worker (não cair se reiniciar)
# /etc/supervisor/conf.d/onsoft-agt-worker.conf
[program:onsoft-agt-worker]
command=php /var/www/html/artisan queue:work --queue=agt-faturas --tries=3
autostart=true
autorestart=true
```

**Variáveis de ambiente mínimas para produção:**

```
APP_ENV=production
APP_DEBUG=false
AGT_AMBIENTE=producao
AGT_SOFTWARE_CHAVE_PRIVADA="-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
AGT_SOFTWARE_NUMERO_CERTIFICACAO=XXXX
CACHE_STORE=redis          # ou database se não tiver Redis
SESSION_DRIVER=redis       # ou database
QUEUE_CONNECTION=redis     # ou database
```

---

*Onsoft AGT v1.5.0 — Adilson Miguel —  — 2068417074*

---

🖨️ Estados no PDF — O que aparece em cada situação
--------------------------------------------------

[](#️-estados-no-pdf--o-que-aparece-em-cada-situação)

O PDF mostra automaticamente banners visuais no topo de acordo com o estado da fatura. Funciona nos três formatos: A4, 88mm e 58mm.

### Banners por estado

[](#banners-por-estado)

EstadoO que aparece no PDFCor`draft` — não submetido⚠️ **DOCUMENTO NÃO SUBMETIDO À AGT** com instrução para submeterAmarelo`pending` — em fila🕐 **EM FILA DE SUBMISSÃO AGT** com UUIDAzul claro`submitted` — aguarda resposta📤 **SUBMETIDO — AGUARDA RESPOSTA DA AGT**Azul`accepted` — aceite ✓Badge verde "AGT: ACCEPTED" no cabeçalhoVerde`rejected` — rejeitado❌ **REJEITADO PELA AGT** com instrução para corrigir e resubmeterVermelho`failed` — erro técnico🔴 **ERRO NA SUBMISSÃO AGT** com comando artisan para retentarRoxo`cancelled` + `payment_status=cancelled`⛔ **DOCUMENTO CANCELADO** com motivo e dataVermelho escuro + marca d'água`NC` (Nota de Crédito)Marca d'água diagonal "NOTA DE CRÉDITO" + cabeçalho vermelhoVermelho### O que cada banner contém

[](#o-que-cada-banner-contém)

**Documento não submetido (`draft`):**

```
⚠️ DOCUMENTO NÃO SUBMETIDO À AGT
Este documento ainda não foi enviado à Administração Geral Tributária.
Para submeter, use o endpoint: POST /onsoft-agt/faturas/1247/submeter
A submissão é obrigatória para documentos com validade fiscal (Decreto Executivo AGT).

```

**Cancelado:**

```
⛔ DOCUMENTO CANCELADO
Este documento foi cancelado e não tem validade fiscal.
Motivo: Erro no valor — reemissão necessária
Data de cancelamento: 18/06/2026 14:30
Nota de Crédito emitida: Ver documento NC associado (ID: 1248)
Conforme AGT: documentos cancelados após submissão requerem Nota de Crédito.

```

**Rejeitado:**

```
❌ REJEITADO PELA AGT
Este documento foi rejeitado pela Administração Geral Tributária.
Acção recomendada: Verificar os dados do documento e resubmeter após correcção.
Use: POST /onsoft-agt/faturas/1247/submeter
Se o erro persistir, emita uma Nota de Crédito e crie uma nova fatura corrigida.

```

**Erro técnico:**

```
🔴 ERRO NA SUBMISSÃO AGT
Ocorreu um erro técnico ao submeter à AGT. O documento não foi recebido.
Acção recomendada: Retentar a submissão.
Use: POST /onsoft-agt/faturas/1247/submeter
Ou via Artisan: php artisan onsoft-agt:retentar-falhas

```

---

📊 Estatísticas AGT por Organização
----------------------------------

[](#-estatísticas-agt-por-organização)

### `GET /onsoft-agt/relatorios/estado-agt`

[](#get-onsoft-agtrelatoriosestado-agt-1)

Estatísticas completas para a organização actual. Inclui totais por estado + últimas 50 submissões.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "draft":     { "total": 5,    "valor": 25000.00 },
    "pending":   { "total": 3,    "valor": 15000.00 },
    "submitted": { "total": 12,   "valor": 60000.00 },
    "accepted":  { "total": 1000, "valor": 5000000.00 },
    "rejected":  { "total": 2,    "valor": 10000.00 },
    "failed":    { "total": 1,    "valor": 5000.00 },
    "cancelled": { "total": 8,    "valor": 40000.00 },
    "taxa_submissao": 97.5,
    "total_documentos": 1031,
    "ultimas_submissoes": [
      {
        "id": 445,
        "invoiceId": 1247,
        "status": "accepted",
        "attempts": 1,
        "submitted_at": "2026-06-18T14:30:05Z",
        "accepted_at":  "2026-06-18T14:30:10Z",
        "rejected_at":  null,
        "error_message": null
      },
      {
        "id": 444,
        "invoiceId": 1246,
        "status": "failed",
        "attempts": 3,
        "submitted_at": "2026-06-18T13:00:00Z",
        "accepted_at":  null,
        "rejected_at":  null,
        "error_message": "Connection timeout — AGT API unreachable"
      }
    ]
  }
}
```

**Como usar no frontend (gráfico donut):**

```
// Recharts / Chart.js
const estados = ['draft','pending','submitted','accepted','rejected','failed','cancelled'];
const cores   = ['#90a4ae','#64b5f6','#42a5f5','#66bb6a','#ef5350','#ab47bc','#ef9a9a'];
const data = estados.map((e, i) => ({
  name:  e.toUpperCase(),
  value: res.data.dados[e].total,
  fill:  cores[i],
}));
```

---

### `GET /onsoft-agt/relatorios/estado-agt-todas-organizacoes`

[](#get-onsoft-agtrelatoriosestado-agt-todas-organizacoes)

Visão admin — estatísticas AGT para TODAS as organizações. Mostra quantas faturas cada escola tem em cada estado.

**Parâmetros:** `?de=2026-01-01&ate=2026-12-31`

**Response 200:**

```
{
  "sucesso": true,
  "dados": [
    {
      "organizationId": 1,
      "draft":     { "total": 0,   "valor": 0 },
      "pending":   { "total": 2,   "valor": 10000 },
      "submitted": { "total": 5,   "valor": 25000 },
      "accepted":  { "total": 450, "valor": 2250000 },
      "rejected":  { "total": 1,   "valor": 5000 },
      "failed":    { "total": 0,   "valor": 0 },
      "cancelled": { "total": 3,   "valor": 15000 },
      "total_documentos": 461,
      "total_valor": 2305000
    },
    {
      "organizationId": 2,
      "draft":     { "total": 5,   "valor": 25000 },
      "pending":   { "total": 1,   "valor": 5000 },
      "submitted": { "total": 7,   "valor": 35000 },
      "accepted":  { "total": 550, "valor": 2750000 },
      "rejected":  { "total": 1,   "valor": 5000 },
      "failed":    { "total": 1,   "valor": 5000 },
      "cancelled": { "total": 5,   "valor": 25000 },
      "total_documentos": 570,
      "total_valor": 2845000
    }
  ]
}
```

**Como usar no frontend (tabela comparativa):**

```
// Tabela com uma linha por organização, colunas por estado
dados.forEach(org => {
  console.log(`Org ${org.organizationId}:
    Aceites:   ${org.accepted.total}
    Em fila:   ${org.pending.total}
    Falhou:    ${org.failed.total}
    Total:     ${org.total_documentos}`);
});
```

---

*Onsoft AGT v1.5.0 — Adilson Miguel —  — 2068417074*

---

👨‍🎓 Área do Aluno — Faturas
---------------------------

[](#‍-área-do-aluno--faturas)

Endpoints para o aluno autenticado consultar as suas próprias faturas.

### Padrão do projecto On-School

[](#padrão-do-projecto-on-school)

O `alunoId` é passado via **query string** (`?alunoId=101`) — exactamente igual ao padrão existente em `EstudanteInfoController`, `EstudanteAnoClasseController` e todos os outros controladores do projecto.

Existem **dois grupos de rotas**:

GrupoURLalunoIdQuem usa`/aluno/``GET /onsoft-agt/aluno/faturas?alunoId=101`Query string obrigatóriaSecretaria, admin, encarregado`/eu/``GET /onsoft-agt/eu/faturas``auth()->id()` automáticoO próprio aluno autenticado### Como funciona o lookup de faturas

[](#como-funciona-o-lookup-de-faturas)

O sistema encontra as faturas do aluno de **3 formas** em paralelo:

```
1. invoice_items.alunoId = alunoId
   → Faturas onde o aluno está mencionado directamente na linha

2. invoices.studentId = alunoId
   → Faturas criadas directamente para este aluno

3. Via billing morph (billing_propinas, billing_transportes, etc.)
   → billing_propinas.alunoId = alunoId → invoiceable_id → invoice_items → invoices

União dos 3 conjuntos → faturas únicas, sem duplicados

```

### Quando o aluno muda de mensalidade (turma/classe)

[](#quando-o-aluno-muda-de-mensalidade-turmaclasse)

O `EstudanteAnoClasse` regista TODAS as mensalidadeIds históricas do aluno. O sistema consulta o histórico completo e inclui faturas de todas as mensalidades anteriores. O aluno vê o historial financeiro completo independentemente de quantas turmas mudou.

### Faturas com múltiplos alunos

[](#faturas-com-múltiplos-alunos)

Quando uma fatura cobre propinas de 2 filhos do mesmo encarregado, cada aluno vê a fatura completa mas com distinção clara:

```
{
  "fatura_partilhada": true,
  "total_alunos_fatura": 2,
  "meus_itens": [
    { "description": "Propina — Outubro 2026", "line_total": 45000 }
  ],
  "outros_itens": [
    { "description": "Propina — Outubro 2026", "alunoId": 102, "line_total": 45000 }
  ],
  "meu_total": 45000,
  "gross_total": 90000
}
```

---

### `GET /onsoft-agt/aluno/faturas?alunoId=101`

[](#get-onsoft-agtalunofaturasalunoid101)

Para secretaria/admin/encarregado. Filtros: `?alunoId=101&de=&ate=&document_type=&payment_status=`

### `GET /onsoft-agt/eu/faturas`

[](#get-onsoft-agteufaturas)

Para o próprio aluno autenticado. Sem alunoId — usa `auth()->id()` automaticamente.

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "aluno_id": 101,
    "total_faturas": 12,
    "resumo": {
      "total_emitido": 540000.00,
      "total_pago": 495000.00,
      "total_divida": 45000.00,
      "total_canceladas": 1,
      "por_estado_agt": { "accepted": 10, "draft": 1, "failed": 1 },
      "por_tipo": { "FR": 10, "FT": 1, "NC": 1 }
    },
    "faturas": [
      {
        "id": 1247,
        "document_no": "FR FR-2026/001247",
        "document_type": "FR",
        "label_tipo": "Fatura-Recibo",
        "issued_at_fmt": "18/06/2026 14:30",
        "payment_status": "paid",
        "agt_status": "accepted",
        "gross_total": 90000.00,
        "paid_total": 90000.00,
        "remaining_balance": 0.00,
        "fatura_partilhada": true,
        "total_alunos_fatura": 2,
        "meu_total": 45000.00,
        "meus_itens": [
          {
            "description": "Propina — Outubro 2026",
            "quantity": 1,
            "unit_price": 45000.00,
            "tax_type": "ISENTO",
            "line_total": 45000.00,
            "item_category": "propina"
          }
        ],
        "outros_itens": [
          {
            "description": "Propina — Outubro 2026",
            "alunoId": 102,
            "line_total": 45000.00
          }
        ],
        "payments": [
          {
            "amount": 90000.00,
            "methods": [
              { "method_code": "NU", "label": "Numerário", "amount": 90000.00 }
            ]
          }
        ],
        "pode_ver_pdf": true,
        "pode_submeter": false,
        "pdf_url": "https://api.escola.ao/onsoft-agt/faturas/1247/pdf",
        "pdf_base64_url": "https://api.escola.ao/onsoft-agt/faturas/1247/pdf-base64",
        "mensalidade_id": 5,
        "hash_control": "mYJE"
      }
    ],
    "mensalidades": [
      {
        "estudante_ano_classe_id": 89,
        "mensalidade_id": 5,
        "status": 1,
        "anolectivo": { "id": 3, "name": "2026" },
        "curso": { "id": 2, "name": "Ensino Geral" },
        "classe": { "id": 4, "name": "10ª Classe" },
        "turma": { "id": 7, "name": "Turma A" },
        "sala": { "id": 2, "name": "Sala 102" },
        "periodo": { "id": 1, "name": "Manhã" },
        "pagamento": {
          "propinaAnual": 540000.00,
          "propinaMensal": 45000.00,
          "confirmacaoPreco": 15000.00,
          "matriculaPreco": 25000.00
        }
      }
    ]
  }
}
```

---

### `GET /onsoft-agt/aluno/faturas/{id}`

[](#get-onsoft-agtalunofaturasid)

Detalhes de uma fatura específica. Retorna 403 se o aluno não tiver acesso.

---

### `GET /onsoft-agt/aluno/faturas/{id}/pdf`

[](#get-onsoft-agtalunofaturasidpdf)

PDF da fatura em stream. Verifica acesso antes de gerar.

---

### `GET /onsoft-agt/aluno/faturas/{id}/pdf-base64`

[](#get-onsoft-agtalunofaturasidpdf-base64)

PDF em base64 para o frontend abrir sem guardar.

```
const { base64 } = res.data.dados;
window.open(`data:application/pdf;base64,${base64}`);
```

---

### `GET /onsoft-agt/aluno/mensalidades`

[](#get-onsoft-agtalunomensalidades)

Histórico de todas as mensalidades do aluno (incluindo mudanças de turma).

---

### `GET /onsoft-agt/aluno/resumo`

[](#get-onsoft-agtalunoresumo)

Resumo financeiro do aluno: total emitido, pago, em dívida, por tipo e estado AGT.

---

### Como integrar no routes/api.php do projecto

[](#como-integrar-no-routesapiphp-do-projecto)

```
use Onsoft\Agt\Http\Controladores\ControladorFaturasAluno;

// ── Para secretaria / admin / encarregado (alunoId na query string) ──
// Mesmo padrão que EstudanteInfoController
Route::middleware(['jwt.auth', SetCurrentOrganization::class])->group(function () {
    Route::get('/aluno/faturas',                 [ControladorFaturasAluno::class, 'faturas']);
    Route::get('/aluno/faturas/{id}',            [ControladorFaturasAluno::class, 'fatura']);
    Route::get('/aluno/faturas/{id}/pdf',        [ControladorFaturasAluno::class, 'pdfFatura']);
    Route::get('/aluno/faturas/{id}/pdf-base64', [ControladorFaturasAluno::class, 'pdfBase64Fatura']);
    Route::get('/aluno/mensalidades',            [ControladorFaturasAluno::class, 'mensalidades']);
    Route::get('/aluno/resumo',                  [ControladorFaturasAluno::class, 'resumo']);
    // Uso: GET /api/aluno/faturas?alunoId=101
    //      GET /api/aluno/faturas/1247?alunoId=101
    //      GET /api/aluno/faturas/1247/pdf?alunoId=101
});

// ── Para o próprio aluno autenticado (sem alunoId — usa auth()->id()) ──
Route::prefix('estudante')
    ->middleware(['jwt.auth', 'org', 'role:estudante'])
    ->group(function () {
        Route::get('/eu/faturas',                 [ControladorFaturasAluno::class, 'minhasFaturas']);
        Route::get('/eu/faturas/{id}',            [ControladorFaturasAluno::class, 'minhaFatura']);
        Route::get('/eu/faturas/{id}/pdf',        [ControladorFaturasAluno::class, 'minhaPdfFatura']);
        Route::get('/eu/faturas/{id}/pdf-base64', [ControladorFaturasAluno::class, 'minhaPdfBase64']);
        Route::get('/eu/mensalidades',            [ControladorFaturasAluno::class, 'minhasMensalidades']);
        Route::get('/eu/resumo',                  [ControladorFaturasAluno::class, 'meuResumo']);
        // Uso: GET /api/estudante/eu/faturas  (sem alunoId)
    });
```

---

*Onsoft AGT v1.6.0 — Adilson Miguel —  — 2068417074*

---

🖼️ Logo da Fatura — Sempre da Base de Dados
-------------------------------------------

[](#️-logo-da-fatura--sempre-da-base-de-dados)

O logótipo no PDF **nunca** vem do `request` — vem sempre de `Organization.logo_path`, resolvido exactamente como o `OrganizationController` já faz:

```
Storage::disk('public')->url($org->logo_path)
```

Isto evita que alguém envie um `logo_url` falso no payload da fatura para tentar inserir conteúdo malicioso ou indevido no documento fiscal.

**Campos do snapshot construídos directamente da tabela `organizations`:**

```
[
    'nif'             => $org->nif,
    'name'            => $org->nome_fiscal,
    'commercial_name' => $org->nome_comercial,
    'address'         => $org->endereco,
    'bairro'          => $org->bairro,
    'city'            => $org->municipio,
    'province'        => $org->provincia,
    'country'         => $org->pais ?? 'Angola',
    'telefone'        => $org->telefone,
    'telefone_alt'    => $org->telefone_alt,
    'email'           => $org->email,
    'website'         => $org->website,
    'logo_path'       => $org->logo_path,
    'logo_url'        => $logoUrl,  // resolvido via Storage::disk('public')->url()
]
```

Se `logo_path` estiver vazio ou o ficheiro não existir, `logo_url` fica `null` e o PDF simplesmente não mostra logo — sem erro.

---

📅 Ordem Sequencial de Pagamento de Propinas
-------------------------------------------

[](#-ordem-sequencial-de-pagamento-de-propinas)

### A regra

[](#a-regra)

Um aluno **nunca** pode pagar o mês 7 sem ter pago 1, 2, 3, 4, 5 e 6 antes. A ordem é definida pelo campo `meses.orderNumber`. Meses com `anularpagamento = true` são ignorados na sequência (não contam).

"Pago" = existe `BillingPropina` com `mensalidadeId` + `alunoId` + `anolectivoId` + `mesid` correctos e `status != 'cancelled'`.

### Como o pacote valida (1 única query, sem N+1)

[](#como-o-pacote-valida-1-única-query-sem-n1)

```
-- Uma única query LEFT JOIN para o ano lectivo completo
SELECT m.id, m.mesId, m.name, m.orderNumber,
       CASE WHEN bp.id IS NOT NULL THEN 1 ELSE 0 END as ja_pago
FROM meses m
LEFT JOIN billing_propinas bp
    ON bp.mesid = m.mesId
   AND bp.mensalidadeId = ?
   AND bp.alunoId = ?
   AND bp.anolectivoId = ?
   AND bp.status != 'cancelled'
WHERE m.anolectivoId = ?
  AND m.anularpagamento = 0
ORDER BY m.orderNumber
```

Depois em memória (sem mais queries):

1. Encontra o `orderNumber` mais alto já pago
2. Verifica que o(s) mês(es) pedido(s) começam exactamente em `último_pago + 1`
3. Verifica que não há buracos se pedir vários meses de uma vez

### Payload de exemplo — item de propina

[](#payload-de-exemplo--item-de-propina)

```
{
  "item_category": "propina",
  "description": "Propina — Julho 2026",
  "quantity": 1,
  "unit_price": 45000.00,
  "tax_code": "ISE",
  "tax_percentage": 0,
  "mensalidadeId": 5,
  "alunoId": 101,
  "anolectivoId": 3,
  "mesId": 47,
  "classComExam": false
}
```

### Erro devolvido se a ordem for violada

[](#erro-devolvido-se-a-ordem-for-violada)

```
{
  "sucesso": false,
  "mensagem": "ORDEM DE PAGAMENTO VIOLADA — Não é possível pagar 'Julho' (posição 7) sem primeiro pagar 'Abril' (posição 4). As propinas devem ser pagas em ordem sequencial: 1, 2, 3... sem saltar meses."
}
```

### Lock atómico — sem afectar outros alunos

[](#lock-atómico--sem-afectar-outros-alunos)

```
BillingPropina::where('mensalidadeId', $mensalidadeId)
    ->where('alunoId', $alunoId)
    ->where('anolectivoId', $anolectivoId)
    ->lockForUpdate()  // ← lock ESTREITO, só estas linhas
    ->get();
```

O lock é **por aluno+mensalidade**, nunca pela tabela inteira. Isto significa que 10.000 alunos diferentes podem pagar propinas em paralelo sem qualquer contenção — apenas dois pedidos *simultâneos do mesmo aluno* esperam um pelo outro (cenário extremamente raro, resolvido em &lt;10ms).

### Endpoints de apoio para o frontend

[](#endpoints-de-apoio-para-o-frontend)

**`GET /onsoft-agt/propinas/proximo-mes?mensalidadeId=5&alunoId=101&anolectivoId=3`**

Devolve o próximo mês que o aluno deve pagar — útil para pré-seleccionar no formulário.

```
{
  "sucesso": true,
  "dados": {
    "proximo_mes": { "id": 47, "mesId": 47, "name": "Julho", "orderNumber": 7, "data": "2026-07-01" },
    "todos_meses_pagos": false
  }
}
```

**`POST /onsoft-agt/propinas/validar-ordem`**

Pré-validar sem criar nada — feedback imediato ao utilizador antes de submeter.

```
// Request
{ "mensalidadeId": 5, "alunoId": 101, "anolectivoId": 3, "mesIds": [7] }

// Response
{ "sucesso": false, "dados": { "pode_pagar": false, "erro": "ORDEM DE PAGAMENTO VIOLADA — ..." } }
```

### Índices de BD adicionados (críticos para performance)

[](#índices-de-bd-adicionados-críticos-para-performance)

```
billing_propinas: (mensalidadeId, alunoId, anolectivoId)
billing_propinas: (mensalidadeId, alunoId, anolectivoId, mesid)
billing_propinas: (mesid, status)
meses:            (anolectivoId, anularpagamento, classComExam)
meses:            (anolectivoId, orderNumber)
estudanteanoclasse: (organizationId, alunoId)
```

Com estes índices, a query de validação executa em **&lt;1ms** mesmo com 100.000+ registos de propinas na BD — essencial para suportar alto volume de pedidos simultâneos.

---

*Onsoft AGT v1.7.0 — Adilson Miguel —  — 2068417074*

---

💳 Exclusividade de Métodos de Pagamento (Multicaixa Express, Referência Multicaixa, POS Online)
-----------------------------------------------------------------------------------------------

[](#-exclusividade-de-métodos-de-pagamento-multicaixa-express-referência-multicaixa-pos-online)

### Importante — este pacote NÃO duplica a infra-estrutura de pagamentos online

[](#importante--este-pacote-não-duplica-a-infra-estrutura-de-pagamentos-online)

O projecto **já tem** o ciclo completo de pagamento online implementado e funcional:

Componente já existente no projectoFunção`App\Models\Config\Financeiro\TipoDePagamento`Tabela `tipodepagamento` com os 10 métodos estáticos (appCode 1001-1010)`App\Models\Payment\OnlinePaymentIntent`Staging da referência/pagamento ANTES da fatura`App\Models\Payment\OrganizationPaymentConfig`Configuração do provider (ProxyPay) por organização`App\Models\Payment\OnlinePaymentWebhookLog`Log de auditoria de cada webhook recebido`App\Services\Payment\OnlinePaymentIntentService``createIntent()` e `markPaidAndCreateInvoice()` — staging + criação atómica da fatura SÓ após confirmação`App\Http\Controllers\PaymentProvider\ProxyPayWebhookController`Recebe o webhook do ProxyPay e chama o service`App\Http\Controllers\PaymentProvider\PaymentReferenceController``POST /payment-references` para iniciar, `cancel`, `refresh`**O pacote Onsoft AGT não recria nada disto.** Continue a usar os endpoints existentes do projecto para o fluxo de pagamento online:

```
POST /api/payment-references           → cria a referência (PaymentReferenceController::store)
POST /api/payment-references/{id}/cancel
POST /webhooks/proxypay                 → confirma e cria a fatura (ProxyPayWebhookController::handle)

```

### O que o pacote adiciona — apenas a regra de exclusividade que faltava

[](#o-que-o-pacote-adiciona--apenas-a-regra-de-exclusividade-que-faltava)

Antes desta versão, nada impedia que Multicaixa Express fosse combinado com Numerário na mesma fatura. O pacote acrescenta **duas colunas** à tabela `tipodepagamento` já existente (migração `2024_01_01_000004_add_exclusivity_to_tipodepagamento.php` — não cria tabela nova):

```
ALTER TABLE tipodepagamento
  ADD COLUMN exclusivo BOOLEAN DEFAULT FALSE AFTER appCode,
  ADD COLUMN requer_consulta_online BOOLEAN DEFAULT FALSE AFTER exclusivo,
  ADD COLUMN provider VARCHAR(60) NULL AFTER requer_consulta_online;

-- Marca automaticamente os 3 métodos exclusivos conhecidos
UPDATE tipodepagamento SET exclusivo = 1, requer_consulta_online = 1, provider = 'proxypay'
WHERE appCode IN (1005, 1006, 1009);
```

appCodeNomeexclusivorequer\_consulta\_onlineprovider1001Dinheirofalsefalse—1002TPAfalsefalse—1003Transferência Bancáriafalsefalse—1004Depósito Bancáriofalsefalse—**1005****Multicaixa Express****true****true**proxypay**1006****Referência Multicaixa****true****true**proxypay1007Chequefalsefalse—1008Carteira Internafalsefalse—**1009****POS Online****true****true**proxypay1010Pagamento Parcialfalsefalse—### Onde a validação corre

[](#onde-a-validação-corre)

`ServicoFatura::criar()` chama `validarExclusividadeMetodos()` em **todo** pedido de criação de fatura com mais de um método de pagamento — lendo directamente de `tipodepagamento.exclusivo`. Isto protege qualquer caminho de criação de fatura, incluindo o `OnlinePaymentIntentService::markPaidAndCreateInvoice()` do projecto, que internamente chama o mesmo `InvoiceService`/`ServicoFatura`.

```
// Rejeitado automaticamente
{
  "payments": [
    { "tipodepagamentoId": 1005, "amount": 30000 },
    { "tipodepagamentoId": 1001, "amount": 20000 }
  ]
}
```

```
{
  "sucesso": false,
  "mensagem": "'Multicaixa Express' é um método de pagamento exclusivo e não pode ser combinado com outros métodos na mesma fatura. Use apenas este método isoladamente."
}
```

### Endpoints leves adicionados pelo pacote

[](#endpoints-leves-adicionados-pelo-pacote)

```
GET  /onsoft-agt/metodos-pagamento           → lista tipodepagamento com exclusivo/provider
POST /onsoft-agt/metodos-pagamento/validar   → validar combinação SEM criar nada (feedback ao frontend)

```

**Exemplo:**

```
POST /onsoft-agt/metodos-pagamento/validar
{ "payments": [{ "tipodepagamentoId": 1005, "amount": 50000 }] }
```

```
{ "sucesso": true, "dados": { "valido": true, "erro": null } }
```

### Extensibilidade — marcar um novo método como exclusivo no futuro

[](#extensibilidade--marcar-um-novo-método-como-exclusivo-no-futuro)

Não é preciso código nenhum. Basta uma linha SQL:

```
UPDATE tipodepagamento SET exclusivo = 1, requer_consulta_online = 1, provider = 'meu_provider'
WHERE appCode = 1011;
```

A validação em `ServicoFatura` passa a aplicar-se automaticamente a esse `appCode`, sem qualquer alteração no pacote.

### `ServicoExclusividadePagamento` — uso directo no código

[](#servicoexclusividadepagamento--uso-directo-no-código)

```
use Onsoft\Agt\Servicos\ServicoExclusividadePagamento;

$servico = app(ServicoExclusividadePagamento::class);

$resultado = $servico->validar($request->input('payments'));
// ['valido' => bool, 'erro' => ?string]

$ehOnline = $servico->ehExclusivoOnline(1005); // true
$provider = $servico->providerDoAppCode(1005); // 'proxypay'
```

---

*Onsoft AGT v1.8.0 — Adilson Miguel —  — 2068417074*

---

🗓️ Mapa de Meses — Pago / Parcial / Pendente
--------------------------------------------

[](#️-mapa-de-meses--pago--parcial--pendente)

### `GET /onsoft-agt/propinas/mapa-meses?mensalidadeId=5&alunoId=101&anolectivoId=3&propinaAnual=540000`

[](#get-onsoft-agtpropinasmapa-mesesmensalidadeid5alunoid101anolectivoid3propinaanual540000)

Mostra, para um aluno+mensalidade+ano lectivo, o estado de **todos os meses** com a `propinaMensal` calculada automaticamente (`propinaAnual / totalMeses` — mesma fórmula do `Mes::paidAndUnpaidMeses()` já usado no projecto).

**Regras aplicadas:**

- Um mês sem registo em `billing_propinas`, ou com `status='cancelled'`, **nunca é ignorado** — é sempre contado na sequência e aparece como `pendente`
- `status='paid'` → `estado: "pago"`
- `status='partial'` → `estado: "parcial"` (conta como ocupando a posição na sequência)
- `pode_pagar_agora` só é `true` no mês que está exactamente na posição seguinte ao último pago/parcial

**Response 200:**

```
{
  "sucesso": true,
  "dados": {
    "resumo": {
      "total_meses": 10,
      "propina_anual": 540000.00,
      "propina_mensal": 54000.00,
      "total_meses_pagos": 4,
      "total_meses_parciais": 1,
      "total_meses_pendentes": 5,
      "total_pago": 216000.00,
      "total_em_divida": 27000.00,
      "proximo_mes_a_pagar": "Maio",
      "proximo_order_number": 6,
      "todos_meses_pagos": false
    },
    "meses": [
      { "mesId": 1, "name": "Janeiro",  "orderNumber": 1, "estado": "pago",     "propina_mensal": 54000.00, "valor_pago": 54000.00, "valor_restante": 0.00,    "pode_pagar_agora": false },
      { "mesId": 2, "name": "Fevereiro","orderNumber": 2, "estado": "pago",     "propina_mensal": 54000.00, "valor_pago": 54000.00, "valor_restante": 0.00,    "pode_pagar_agora": false },
      { "mesId": 3, "name": "Março",    "orderNumber": 3, "estado": "pago",     "propina_mensal": 54000.00, "valor_pago": 54000.00, "valor_restante": 0.00,    "pode_pagar_agora": false },
      { "mesId": 4, "name": "Abril",    "orderNumber": 4, "estado": "pago",     "propina_mensal": 54000.00, "valor_pago": 54000.00, "valor_restante": 0.00,    "pode_pagar_agora": false },
      { "mesId": 5, "name": "Maio",     "orderNumber": 5, "estado": "parcial",  "propina_mensal": 54000.00, "valor_pago": 27000.00, "valor_restante": 27000.00,"pode_pagar_agora": true  },
      { "mesId": 6, "name": "Junho",    "orderNumber": 6, "estado": "pendente", "propina_mensal": 54000.00, "valor_pago": 0.00,     "valor_restante": 54000.00,"pode_pagar_agora": false },
      { "mesId": 7, "name": "Julho",    "orderNumber": 7, "estado": "pendente", "propina_mensal": 54000.00, "valor_pago": 0.00,     "valor_restante": 54000.00,"pode_pagar_agora": false }
    ]
  }
}
```

> Note: no exemplo acima, Maio está `parcial`, por isso `pode_pagar_agora` fica `true` em Maio (precisa de terminar de pagar essa posição) e `false` em Junho — não se pode saltar para Junho enquanto Maio não estiver `pago` por completo.

Útil para o frontend desenhar uma grelha de 12 meses com cores (verde=pago, amarelo=parcial, cinza=pendente) e bloquear o clique em meses fora de ordem.

---

*Onsoft AGT v1.9.0 — Adilson Miguel —  — 2068417074*

---

🔄 Alternância: Faturação Eletrónica AGT ⇄ SAF-T (AO)
----------------------------------------------------

[](#-alternância-faturação-eletrónica-agt--saf-t-ao)

### O que isto resolve

[](#o-que-isto-resolve)

Algumas organizações precisam de operar sob o regime **SAF-T (AO)** em vez do regime de **Faturação Eletrónica em tempo real**. O pacote permite alternar entre os dois a qualquer momento — **de forma totalmente reversível, em ambos os sentidos**.

ModoComportamento`electronic` (defeito)Cada fatura é assinada (RSA-SHA1), recebe hash chain, jws e QR Code, e é submetida em tempo real à AGT`saft_ao`Faturas continuam a ser criadas normalmente, mas **sem** assinatura nem submissão em tempo real. Periodicamente é exportado um ficheiro XML SAF-T(AO) com todas as faturas de um intervalo de datas**A troca nunca apaga ou altera faturas já emitidas** — afecta apenas o comportamento das faturas criadas a partir do momento da troca.

---

### Consultar o modo actual

[](#consultar-o-modo-actual)

```
GET /onsoft-agt/modo-faturacao/estado

```

```
{
  "sucesso": true,
  "dados": {
    "modo_actual": "electronic",
    "modo_label": "Faturação Eletrónica AGT",
    "submissao_tempo_real_activa": true,
    "requer_geracao_saft": false,
    "alterado_em": null
  }
}
```

### Alternar para SAF-T (AO)

[](#alternar-para-saf-t-ao)

```
POST /onsoft-agt/modo-faturacao/alternar
{ "modo": "saft_ao" }

```

```
{
  "sucesso": true,
  "dados": {
    "alterado": true,
    "modo_anterior": "electronic",
    "modo_actual": "saft_ao",
    "mensagem": "Organização alternada para regime SAF-T (AO). Faturas a partir de agora deixam de ser submetidas em tempo real à AGT."
  }
}
```

### Voltar para Faturação Eletrónica — vice-versa, sempre possível

[](#voltar-para-faturação-eletrónica--vice-versa-sempre-possível)

```
POST /onsoft-agt/modo-faturacao/alternar
{ "modo": "electronic" }

```

A validação garante que a organização só pode activar `saft_ao` se já tiver `tax_registration_number` (NIF) e `software_validation_number` configurados — senão devolve erro `422` explicando o que falta.

---

📄 Geração do Ficheiro SAF-T (AO) — entre Data de Início e Data de Fim
---------------------------------------------------------------------

[](#-geração-do-ficheiro-saf-t-ao--entre-data-de-início-e-data-de-fim)

### Pré-visualizar (sem gerar o ficheiro)

[](#pré-visualizar-sem-gerar-o-ficheiro)

```
GET /onsoft-agt/saft/previsualizar?data_inicio=2026-06-01&data_fim=2026-06-30

```

```
{
  "sucesso": true,
  "dados": {
    "data_inicio": "2026-06-01",
    "data_fim": "2026-06-30",
    "total_faturas": 412,
    "total_emitido": 18540000.00,
    "total_iva": 980000.00,
    "por_tipo": [
      { "tipo": "FR", "total": 390, "valor": 17550000.00 },
      { "tipo": "NC", "total": 22,  "valor": 990000.00 }
    ]
  }
}
```

### Exportar o ficheiro XML directamente (download)

[](#exportar-o-ficheiro-xml-directamente-download)

```
GET /onsoft-agt/saft/exportar?data_inicio=2026-06-01&data_fim=2026-06-30

```

Devolve `Content-Type: application/xml` como anexo (`SAFT_AO___a_.xml`), pronto para entrega à AGT pelo canal próprio do regime SAF-T.

### Exportar em base64 (para SPA/frontend)

[](#exportar-em-base64-para-spafrontend)

```
GET /onsoft-agt/saft/exportar-base64?data_inicio=2026-06-01&data_fim=2026-06-30

```

```
{
  "sucesso": true,
  "dados": {
    "base64": "PD94bWwgdmVyc2lvbj0iMS4wIi...",
    "nome_ficheiro": "SAFT_AO_500000000_20260601_a_20260630.xml",
    "mime_type": "application/xml",
    "total_documentos": 412,
    "resumo": { "...": "..." }
  }
}
```

```
// Frontend — descarregar o XML a partir do base64
const { base64, nome_ficheiro } = res.data.dados;
const blob = new Blob([atob(base64)], { type: 'application/xml' });
const url  = URL.createObjectURL(blob);
const a    = document.createElement('a');
a.href = url; a.download = nome_ficheiro; a.click();
```

---

### Estrutura do XML gerado

[](#estrutura-do-xml-gerado)

```

                   NIF, nome, FiscalYear, StartDate, EndDate, ProductID...

                  Clientes únicos referenciados no período
                    Artigos/serviços únicos referenciados
              Taxas de IVA usadas (incluindo isenções)

      NumberOfEntries, TotalCredit
       (uma por documento)
         (uma por item, com Tax aninhado)
             TaxPayable, NetTotal, GrossTotal

```

Regras aplicadas, consistentes com o resto do pacote:

- NC usa `DebitAmount`; FT/FR/FS/ND usam `CreditAmount`
- `InvoiceStatus = 'A'` para faturas com `payment_status = cancelled`
- Faturas `cancelled` continuam a aparecer no SAF-T (estado Anulado) — nunca são omitidas
- `TaxAccountingBasis` e `CompanyID` configuráveis por organização (colunas `saft_company_id`, `saft_tax_accounting_basis`)

---

### Uso directo no código

[](#uso-directo-no-código)

```
use Onsoft\Agt\Servicos\ServicoModoFaturacao;
use Onsoft\Agt\Servicos\ServicoSaftAo;

$modo = app(ServicoModoFaturacao::class);
$modo->alternarModo($organizacaoId, ServicoModoFaturacao::SAFT_AO, auth()->id());

$saft = app(ServicoSaftAo::class);
$resultado = $saft->gerar($organizacaoId, '2026-06-01', '2026-06-30');
file_put_contents($resultado['nome_ficheiro'], $resultado['xml']);
```

---

*Onsoft AGT v1.10.0 — Adilson Miguel —  — 2068417074*

---

⚠️ Faturas SAF-T NUNCA migram para submissão em tempo real
----------------------------------------------------------

[](#️-faturas-saf-t-nunca-migram-para-submissão-em-tempo-real)

**Resposta directa: não.** Se gerares faturas em modo `saft_ao` e depois voltares para `electronic`, essas faturas antigas **não podem** ser submetidas retroactivamente à Faturação Eletrónica.

### Porquê

[](#porquê)

RazãoExplicaçãoHash chain partidoFaturas SAF-T não têm `invoice_hash` — submeter uma sem hash quebraria a cadeia sequencial da sérieDuplo reporte fiscalA fatura já será (ou já foi) reportada à AGT via ficheiro SAF-T — submeter também em tempo real reportaria o mesmo documento duas vezesPrazo expiradoA submissão em tempo real exige que aconteça no momento da emissão — não retroactivamente, dias ou semanas depois### O que acontece na prática

[](#o-que-acontece-na-prática)

```
Modo: electronic → Fatura A criada (hash, jws, submetida em tempo real)
Modo: saft_ao     → Fatura B, C, D criadas (sem hash, agt_status = saft_pending_export)
[Exportar SAF-T]   → B, C, D ficam agt_status = saft_exported
Modo: electronic   → Fatura E criada (hash, jws, submetida em tempo real — normal)

Resultado:
  A → já submetida (electronic)
  B, C, D → reportadas apenas via SAF-T, nunca via submissão em tempo real
  E → submetida normalmente (electronic)

```

Se tentar chamar `POST /onsoft-agt/faturas/{id}/submeter` numa fatura B, C ou D:

```
{
  "sucesso": false,
  "mensagem": "Esta fatura [FR FR-2026/000412] foi criada em modo SAF-T(AO) e não pode ser submetida à Faturação Eletrónica retroactivamente. Documentos emitidos em regime SAF-T devem ser reportados exclusivamente via exportação do ficheiro SAF-T(AO) (GET /onsoft-agt/saft/exportar)."
}
```

### Auditoria da transição

[](#auditoria-da-transição)

```
GET /onsoft-agt/modo-faturacao/auditoria

```

```
{
  "sucesso": true,
  "dados": {
    "modo_actual": "electronic",
    "faturas_electronicas_total": 1204,
    "faturas_saft_aguardando_exportacao": 0,
    "faturas_saft_ja_exportadas": 47,
    "nota": "Não há faturas SAF-T pendentes de exportação."
  }
}
```

Use este endpoint **antes** de mudar de modo para confirmar que todas as faturas SAF-T pendentes já foram exportadas e entregues à AGT — evita esquecer um período sem ficheiro gerado.

---

*Onsoft AGT v1.10.1 — Adilson Miguel —  — 2068417074*

---

🏷️ Separador Explícito entre Regimes — `invoicing_mode` na Fatura
-----------------------------------------------------------------

[](#️-separador-explícito-entre-regimes--invoicing_mode-na-fatura)

### O problema que isto resolve

[](#o-problema-que-isto-resolve)

Até à v1.10.1, o regime de uma fatura só era identificável indirectamente através de valores específicos de `agt_status` (`saft_pending_export`, `saft_exported`). Isto era ambíguo e não dava ao frontend um campo único e estável para separar visualmente os dois regimes nem para decidir, sem lógica própria, que botões mostrar.

### A solução — coluna dedicada e imutável

[](#a-solução--coluna-dedicada-e-imutável)

A migração `2024_01_01_000006_add_invoicing_mode_to_invoices.php` adiciona `invoicing_mode` directamente à tabela `invoices`. É gravado **uma vez**, no momento da criação, reflectindo o modo da organização nesse instante exacto — e está protegido pelo `InvoiceSnapshotGuard` como **campo imutável**. Mudar o modo da organização no futuro nunca altera o `invoicing_mode` de faturas já criadas.

```
ALTER TABLE invoices ADD COLUMN invoicing_mode VARCHAR(20) DEFAULT 'electronic' AFTER agt_status;
-- Preenchimento retroactivo automático com base no agt_status existente
```

### `ServicoFlagsUiFatura` — fonte única da verdade para o frontend

[](#servicoflagsuifatura--fonte-única-da-verdade-para-o-frontend)

Em vez do frontend decidir com lógica própria que botões mostrar, o backend devolve um objecto de flags já calculado:

```
$flags = app(\Onsoft\Agt\Servicos\ServicoFlagsUiFatura::class)->calcular($fatura);
```

```
{
  "invoicing_mode": "saft_ao",
  "invoicing_mode_label": "SAF-T (AO)",
  "badge_cor": "amber",
  "mostrar_botao_submeter": false,
  "pode_submeter": false,
  "motivo_submeter_desactivado": "Fatura emitida em regime SAF-T(AO) — reportada apenas via exportação do ficheiro SAF-T, nunca em tempo real.",
  "mostrar_botao_retentar": false,
  "mostrar_botao_exportar_saft": true,
  "pode_exportar_saft": true,
  "ja_exportada_saft": false,
  "mostrar_botao_cancelar": true,
  "gera_nota_credito_ao_cancelar": false,
  "pode_editar_pagamento": true,
  "mostra_aviso_regime_misto": false
}
```

**Uso directo no frontend:**

```

  Submeter à AGT

{flags.mostrar_botao_exportar_saft && (
  Exportar SAF-T
)}

{flags.invoicing_mode_label}
```

### Endpoints

[](#endpoints)

**`GET /onsoft-agt/faturas/{id}/estado`** — agora inclui `invoicing_mode` e o bloco `ui` completo:

```
{
  "sucesso": true,
  "dados": {
    "fatura_id": 1247,
    "invoicing_mode": "electronic",
    "agt_status": "accepted",
    "submissao": { "...": "..." },
    "ui": { "invoicing_mode": "electronic", "mostrar_botao_submeter": true, "pode_submeter": false, "...": "..." }
  }
}
```

**`GET /onsoft-agt/faturas/flags-ui?ids=101,102,103`** — flags em massa para tabelas/listagens, sem 1 pedido por linha:

```
{
  "sucesso": true,
  "dados": [
    { "invoice_id": 101, "invoicing_mode": "electronic", "pode_submeter": true, "...": "..." },
    { "invoice_id": 102, "invoicing_mode": "saft_ao", "pode_submeter": false, "...": "..." }
  ]
}
```

### Bloqueio reforçado a nível de servidor

[](#bloqueio-reforçado-a-nível-de-servidor)

`POST /onsoft-agt/faturas/{id}/submeter` agora verifica `invoicing_mode` **antes** de qualquer chamada à API AGT — uma fatura SAF-T nunca chega a tocar no `ServicoSubmissao`:

```
{
  "sucesso": false,
  "mensagem": "Esta fatura foi criada em modo SAF-T(AO) e não pode ser submetida à Faturação Eletrónica. Reporte-a via exportação do ficheiro SAF-T (GET /onsoft-agt/saft/exportar).",
  "ui": { "invoicing_mode": "saft_ao", "pode_submeter": false, "...": "..." }
}
```

Como a resposta de erro já inclui o bloco `ui`, o frontend pode actualizar o estado dos botões imediatamente sem precisar de um segundo pedido a `/estado`.

---

*Onsoft AGT v1.11.0 — Adilson Miguel —  — 2068417074*

---

📑 Original vs Cópia do Documento Original
-----------------------------------------

[](#-original-vs-cópia-do-documento-original)

### A regra AGT

[](#a-regra-agt)

> **Decreto Executivo AGT, Anexo I, ponto 6, alínea n):** "A impressão de uma 2.ª via de um documento deve preservar o seu conteúdo original, ainda que deva conter qualquer expressão que indique não se tratar de um original."
>
> **Alínea h):** "...deverá fazer menção desta qualidade, através da expressão **'Cópia do documento original'** (sem aspas)..."

### Implementação

[](#implementação)

A **primeira** vez que o PDF de uma fatura é gerado — em qualquer canal (`pdf`, `pdf-base64`, `pdf-snapshot`) — é registada como **Original**. Qualquer geração seguinte da mesma fatura, mesmo que seja noutro formato de papel ou canal diferente, é automaticamente marcada **"Cópia do documento original"**.

O conteúdo (valores, NIF, hash, QR Code) é **sempre idêntico** entre Original e Cópia — apenas a etiqueta impressa muda.

### Onde aparece no PDF

[](#onde-aparece-no-pdf)

Em todos os 3 formatos (A4, 88mm, 58mm):

- **Badge** logo abaixo do número do documento, com cor verde para Original e âmbar para Cópia
- **Frase formal no rodapé**, exactamente no formato exigido pela alínea h): `Cópia do documento original — FR FR-2026/001247`

### Lock atómico — sem corridas

[](#lock-atómico--sem-corridas)

Registado via `lockForUpdate()` na tabela `onsoft_agt_invoice_print_log`. Se dois pedidos chegarem ao mesmo tempo para a mesma fatura (ex: duplo clique, ou impressão automática + visualização manual), **nunca** os dois ficam marcados como Original.

### Endpoint de auditoria

[](#endpoint-de-auditoria)

```
GET /onsoft-agt/faturas/{id}/historico-impressao

```

```
{
  "sucesso": true,
  "dados": {
    "fatura_id": 1247,
    "ja_tem_original": true,
    "historico": [
      { "is_original": true,  "via_label": "Original",                     "formato_papel": "A4",   "canal": "pdf",        "gerado_por": 12, "gerado_em": "2026-06-18T14:30:05Z" },
      { "is_original": false, "via_label": "Cópia do documento original",  "formato_papel": "88mm", "canal": "pdf",        "gerado_por": 3,  "gerado_em": "2026-06-19T09:12:40Z" },
      { "is_original": false, "via_label": "Cópia do documento original",  "formato_papel": "A4",   "canal": "pdf-base64", "gerado_por": 12, "gerado_em": "2026-06-20T11:00:00Z" }
    ]
  }
}
```

Permite saber exactamente quem viu/imprimiu o documento, em que formato, e quando — útil para auditoria interna além da exigência fiscal.

---

*Onsoft AGT v1.12.0 — Adilson Miguel —  — 2068417074*

---

🛡️ Default Seguro Quando o Modo Está "Desligado"
------------------------------------------------

[](#️-default-seguro-quando-o-modo-está-desligado)

### O problema que isto resolve

[](#o-problema-que-isto-resolve-1)

Antes da v1.14.0, se uma organização **não tivesse nenhuma configuração AGT**, ou tivesse `agt_enabled = false` (AGT desligado), o sistema assumia silenciosamente o modo `electronic` — tentando assinar e submeter faturas a uma integração que não estava configurada. Isto causava falhas confusas ou, em pior caso, hash inválido persistido na fatura.

### A correcção

[](#a-correcção)

**`ServicoModoFaturacao::modoActual()`** agora segue esta regra de resolução, por ordem:

SituaçãoModo resolvidoSem nenhum registo em `organization_agt_configs``saft_ao``agt_enabled = false``saft_ao``agt_enabled = true` e `invoicing_mode` nunca definido`electronic` (compatibilidade histórica)`agt_enabled = true` e `invoicing_mode` definido explicitamenteusa o valor gravado**Nunca mais assume `electronic` silenciosamente quando não há nada configurado.** Faturas criadas nestas condições não tentam assinatura nem submissão — ficam em modo SAF-T à espera de exportação.

### Escolher o modo explicitamente — mesmo sem configuração prévia

[](#escolher-o-modo-explicitamente--mesmo-sem-configuração-prévia)

```
POST /onsoft-agt/modo-faturacao/alternar
{ "modo": "saft_ao" }

```

Se a organização não tiver **nenhuma** configuração AGT ainda, este pedido **cria automaticamente** um registo `organization_agt_configs` com `invoicing_mode = saft_ao` — sem exigir mais nada. SAF-T(AO) nunca é bloqueado por falta de configuração.

```
POST /onsoft-agt/modo-faturacao/alternar
{ "modo": "electronic" }

```

Activar `electronic` agora **exige** que a organização já tenha NIF, número de certificação **e chave privada do contribuinte** configurados — é este modo que assina documentos, por isso é o que exige preparação completa. Se faltar algo:

```
{
  "sucesso": false,
  "mensagem": "Não é possível activar a Faturação Eletrónica - configuração incompleta: Chave privada do contribuinte não configurada — necessária para assinar documentos. A organização permanece em modo SAF-T(AO) até que isto seja resolvido."
}
```

Ao activar `electronic` com sucesso, `agt_enabled` é automaticamente definido como `true` — escolher o modo é a intenção clara de ligar a submissão em tempo real.

### `GET /onsoft-agt/modo-faturacao/estado` — campos novos

[](#get-onsoft-agtmodo-faturacaoestado--campos-novos)

```
{
  "sucesso": true,
  "dados": {
    "modo_actual": "saft_ao",
    "modo_label": "SAF-T (AO)",
    "pode_alternar": true,
    "configuracao_existe": false,
    "agt_enabled": false,
    "modo_e_default_automatico": true,
    "alterado_em": null,
    "submissao_tempo_real_activa": false,
    "requer_geracao_saft": true
  }
}
```

`modo_e_default_automatico: true` indica que este é o valor por defeito (ninguém escolheu explicitamente) — útil para o frontend mostrar um aviso "Configure o modo de faturação" em vez de assumir que está tudo certo.

---

*Onsoft AGT v1.14.0 — Adilson Miguel —  — 2068417074*

---

🔍 Auditoria de Seguimento (v1.14.1) — 3 Lacunas Novas Encontradas e Corrigidas
------------------------------------------------------------------------------

[](#-auditoria-de-seguimento-v1141--3-lacunas-novas-encontradas-e-corrigidas)

Após a correcção do default seguro SAF-T na v1.14.0, uma nova ronda de auditoria identificou três pontos onde o resto do pacote ainda não reflectia correctamente o regime SAF-T num documento sem hash. Nenhuma destas era um problema antes da v1.14.0 existir — são consequências directas de ter introduzido o default automático.

### 1. Linha de certificação do PDF confundia "SAF-T" com "documento incompleto"

[](#1-linha-de-certificação-do-pdf-confundia-saf-t-com-documento-incompleto)

**Antes:** uma fatura em modo `saft_ao` (que nunca terá hash, por desenho) mostrava no PDF a frase "Aguardando certificação" — sugerindo um estado transitório de erro, quando na realidade é o comportamento permanente e correcto desse regime.

**Depois:** `ServicoPdf::construirLinhaCertificacao()` distingue agora os dois casos:

```
SAF-T(AO):     "Documento emitido sob o regime SAF-T(AO) — nº 0000/AGT.
                Reportado via ficheiro SAF-T(AO), sem assinatura individual por documento."

Electronic sem hash ainda: "Aguardando certificação — nº 0000/AGT" (inalterado)

```

### 2. `invoicing_mode` da fatura não chegava ao PDF

[](#2-invoicing_mode-da-fatura-não-chegava-ao-pdf)

**Causa raiz:** `InvoiceObserver` (que constrói o snapshot imutável) e `ServicoPdf::normalizarInvoice()` (que normaliza dados live) nunca incluíam o campo `invoicing_mode` no array entregue às views. A correcção do ponto 1 não tinha nenhum dado para funcionar.

**Corrigido em dois pontos:**

- `InvoiceObserver` — `invoicing_mode` agora gravado no snapshot imutável
- `ServicoPdf::normalizarInvoice()` — `invoicing_mode` agora incluído nos dados live

### 3. QR Code mostrava `HASH:` vazio em faturas SAF-T

[](#3-qr-code-mostrava-hash-vazio-em-faturas-saf-t)

**Antes:** o conteúdo do QR Code de uma fatura SAF-T continha `HASH:` sem valor nenhum, sem explicação — parecia um erro de geração.

**Depois:** `ServicoQrCode::construirConteudo()` substitui o campo `HASH` por `REGIME:SAFT_AO` quando aplicável, comunicando correctamente que a verificação deste documento passa pelo ficheiro SAF-T periódico, não por hash individual.

### 4. Exportação SAF-T falhava para organizações sem configuração

[](#4-exportação-saf-t-falhava-para-organizações-sem-configuração)

**Antes:** `ServicoSaftAo::gerar()` exigia uma linha em `organization_agt_configs` para funcionar — mas a v1.14.0 tornou legítimo uma organização estar em modo `saft_ao` **sem nenhuma configuração**. Isto bloqueava exactamente o caso de uso que a v1.14.0 veio resolver.

**Corrigido:** `gerar()` usa agora uma instância `OrganizationAgtConfig` vazia (não persistida) como fallback neutro quando não existe configuração, preenchendo o XML com os campos disponíveis da `Organization` e defaults seguros (`TaxAccountingBasis = 'F'`).

---

*Onsoft AGT v1.14.1 — Adilson Miguel —  — 2068417074*

---

🚨 Auditoria Profunda (v1.14.2) — Lacuna Crítica Encontrada e Corrigida
----------------------------------------------------------------------

[](#-auditoria-profunda-v1142--lacuna-crítica-encontrada-e-corrigida)

Uma terceira ronda de auditoria, seguindo o fluxo de dados ponta-a-ponta em vez de requisito-a-requisito, encontrou **uma lacuna crítica** e três inconsistências de relatório. Esta foi a ronda mais importante até agora.

### 🔴 Crítico — Submissão automática via fila contornava toda a protecção SAF-T

[](#-crítico--submissão-automática-via-fila-contornava-toda-a-protecção-saf-t)

**Esta é a lacuna mais grave encontrada em todas as rondas de auditoria.**

**O problema:** `ServicoFatura::criar()` despachava `App\Jobs\SubmitInvoiceToAgtJob` — o Job **do projecto hospedeiro**, não do pacote — para a submissão automática (`auto_submit_invoices = true`). Esse Job chama `App\Services\Agt\AgtInvoiceSubmissionService`, um serviço escrito **antes de o regime SAF-T(AO) existir**, sem qualquer noção desse modo.

**Consequência real:** todas as protecções documentadas nas versões anteriores — bloqueio de faturas SAF-T em `ServicoSubmissao::submeter()`, validação no endpoint HTTP — **nunca eram exercidas no fluxo automático de fila**. Se uma organização tivesse `auto_submit_invoices = true`, uma fatura criada em modo SAF-T podia ser silenciosamente submetida à AGT em tempo real através deste caminho alternativo, apesar de toda a documentação anterior garantir o contrário.

**Correcção:** novo Job próprio do pacote, `Onsoft\Agt\Jobs\SubmeterFaturaAgtJob`, com a mesma verificação explícita de `invoicing_mode` usada nos endpoints HTTP. `ServicoFatura::criar()` e `gerarNotaCredito()` foram actualizados para usar este Job em vez do legado.

```
// Onsoft\Agt\Jobs\SubmeterFaturaAgtJob::handle()
if (($fatura->invoicing_mode ?? ServicoModoFaturacao::ELECTRONIC) === ServicoModoFaturacao::SAFT_AO) {
    Log::warning('SubmeterFaturaAgtJob recebeu fatura em modo SAF-T(AO) — ignorado.');
    return;
}
```

**Acção necessária:** `app/Jobs/SubmitInvoiceToAgtJob.php` passa de `KEEP` para `DELETE` na lista de ficheiros a remover após instalação — ver `FICHEIROS_A_ELIMINAR.txt` actualizado.

### Inconsistências de relatório — faturas SAF-T invisíveis nas estatísticas

[](#inconsistências-de-relatório--faturas-saf-t-invisíveis-nas-estatísticas)

**`ServicoRelatorios::estadoAgt()`** tinha categorias fixas (`draft`, `pending`, `submitted`, `accepted`, `rejected`, `failed`, `cancelled`) que não incluíam `saft_pending_export` nem `saft_exported`. O `total_documentos` somava todos os estados (incluindo SAF-T), mas as categorias visíveis ao frontend não — a soma das partes não batia com o total apresentado.

**Corrigido:** adicionadas as duas categorias SAF-T explicitamente, mais `total_documentos_electronic` e `total_documentos_saft` para clareza.

**`calcularTaxaSubmissao()`** dividia faturas aceites pelo total de **todas** as faturas, incluindo SAF-T — distorcendo a métrica para organizações com volume SAF-T elevado (a taxa parecia artificialmente baixa, quando a métrica simplesmente não se aplica a esse regime).

**Corrigido:** o denominador exclui agora `saft_pending_export` e `saft_exported`.

**`estadoAgtTodasOrganizacoes()`** (visão multi-tenant) tinha o mesmo problema de categorias fixas sem SAF-T — corrigido da mesma forma.

### Cancelamento de faturas SAF-T não gerava NC quando devia

[](#cancelamento-de-faturas-saf-t-não-gerava-nc-quando-devia)

**`ServicoFatura::cancelar()`** determinava a necessidade de NC verificando apenas estados exclusivos do regime electronic (`enviado`, `aceite`, `submitted`, `accepted`). Uma fatura SAF-T com `agt_status = saft_exported` — já reportada à AGT via ficheiro — caía sempre no ramo de cancelamento simples, sem gerar NC, apesar de já ter sido comunicada à AGT.

**Corrigido:** a lógica agora bifurca pelo `invoicing_mode` da fatura — SAF-T exige NC quando `saft_exported`; electronic continua a exigir NC nos estados que já tinha.

**`gerarNotaCredito()`** também chamava `gerarEGuardarHashChain()` incondicionalmente, atribuindo hash a NCs de faturas SAF-T. Corrigido para espelhar a mesma condicional usada na criação de faturas normais.

---

*Onsoft AGT v1.14.2 — Adilson Miguel —  — 2068417074*

---

🔴 Auditoria Profunda — Quarta Ronda (v1.14.3) — Lacuna Fundamental na Imutabilidade
-----------------------------------------------------------------------------------

[](#-auditoria-profunda--quarta-ronda-v1143--lacuna-fundamental-na-imutabilidade)

Esta foi a ronda de auditoria mais profunda até agora — em vez de verificar requisito a requisito, segui a execução completa do código (fatura → cancelamento → NC → reset de ano fiscal → relatórios) e procurei especificamente em jobs, comandos e observers, áreas que escapam à verificação directa de endpoints HTTP.

### 🔴 Crítico — Faturas SAF-T nunca tinham protecção de imutabilidade

[](#-crítico--faturas-saf-t-nunca-tinham-protecção-de-imutabilidade)

**Esta é a lacuna mais fundamental encontrada em todas as quatro rondas de auditoria.**

**A causa raiz:** `InvoiceObserver::created()` só criava o snapshot imutável quando `invoice_hash` não estava vazio. Como faturas SAF-T **nunca** têm hash por desenho desse regime, nunca recebiam snapshot. E como `InvoiceSnapshotGuard::permitirMutacao()` e `verificarAntesDeAtualizar()` usavam apenas `empty($invoice->invoice_hash)` para decidir se a fatura estava "bloqueada", **qualquer campo de uma fatura SAF-T — valores, NIF do cliente, data de emissão, tudo — podia ser alterado livremente em qualquer momento após a criação**, sem qualquer aviso ou bloqueio.

Isto viola directamente o ponto 12.l do Anexo I, que não distingue entre regimes de submissão: um documento fiscal emitido é imutável, independentemente de ser reportado em tempo real ou via ficheiro periódico.

**Correcção em três pontos:**

1. `InvoiceObserver::created()` cria agora o snapshot para **todas** as faturas, sem condição de hash.
2. `InvoiceSnapshotGuard::verificarAntesDeAtualizar()` usa `estaLocked()` (que considera hash OU snapshot) em vez de testar apenas `invoice_hash`.
3. `InvoiceSnapshotGuard::permitirMutacao()` corrigido da mesma forma.

```
// Antes — nunca protegia faturas SAF-T
if (empty($invoice->invoice_hash)) {
    return; // SAF-T cai sempre aqui — sem protecção nenhuma
}

// Depois — protege qualquer regime com snapshot
if (!self::estaLocked($invoice)) {
    return; // só passa livre se NÃO houver hash NEM snapshot
}
```

### Comandos Artisan não reconheciam organizações em modo SAF-T por default automático

[](#comandos-artisan-não-reconheciam-organizações-em-modo-saf-t-por-default-automático)

Três comandos filtravam organizações exclusivamente por `OrganizationAgtConfig.agt_enabled = true`, ignorando por completo organizações sem nenhuma configuração (o novo default seguro SAF-T introduzido na v1.14.0):

- **`onsoft-agt:reset-ano-fiscal --todas-orgs`** — o mais grave dos três, porque corre automaticamente via scheduler todos os anos a 1 de Janeiro. Organizações sem config nunca tinham as suas séries fiscais reiniciadas, ficando indefinidamente paradas no ano anterior.
- **`onsoft-agt:estado`** — ferramenta de diagnóstico que escondia exactamente as organizações que mais precisam de visibilidade (as que nunca configuraram nada).
- **`onsoft-agt:retentar-falhas`** — ainda despachava o Job antigo do projecto (`App\Jobs\SubmitInvoiceToAgtJob`), que a v1.14.2 tinha substituído em `ServicoFatura` mas esquecido aqui.

**Correcção:** os três comandos agora incluem explicitamente organizações com faturas emitidas mesmo sem configuração AGT, e o comando de retry usa o Job próprio do pacote com protecção SAF-T.

### Nota sobre `onsoft-agt:sincronizar-series`

[](#nota-sobre-onsoft-agtsincronizar-series)

Este comando **continua** a filtrar apenas por `agt_enabled = true` — e está correcto fazê-lo. Sincronizar séries com a API AGT só tem sentido para organizações em regime electronic; uma organização SAF-T não tem API em tempo real para sincronizar.

---

*Onsoft AGT v1.14.3 — Adilson Miguel —  — 2068417074*

---

🔴 Auditoria Profunda — Quinta Ronda (v1.14.4) — Snapshots Vazios Desde a Origem
-------------------------------------------------------------------------------

[](#-auditoria-profunda--quinta-ronda-v1144--snapshots-vazios-desde-a-origem)

Esta ronda seguiu a recomendação da ronda anterior — relatórios financeiros e billing morph — e confirmou que ambas as áreas estão correctas. Mas ao verificar a ordem exacta de execução de `ServicoFatura::criar()` para validar a correcção da v1.14.3, foi descoberta uma falha **mais antiga e mais grave** do que qualquer uma das anteriores.

### 🔴 Crítico — O snapshot imutável estava sempre vazio, em qualquer regime

[](#-crítico--o-snapshot-imutável-estava-sempre-vazio-em-qualquer-regime)

**A causa:** o evento `created` do Eloquent dispara imediatamente após o `INSERT` da fatura — **dentro** da mesma transacção, não depois do commit, contrariamente ao que um comentário no código (incorrectamente) afirmava. `ServicoFatura::criar()` cria a fatura primeiro, e só **depois**, em passos seguintes da mesma transacção, persiste os itens (`InvoiceItem`), os pagamentos (`InvoicePayment`) e o hash AGT.

**Consequência:** desde que a criação automática do snapshot foi introduzida, `InvoiceObserver::created()` construía o snapshot **no momento exacto em que a fatura era inserida** — antes de qualquer item ou pagamento existir na base de dados. O array `items` e `payments` do snapshot ficavam sempre vazios `[]`, e o campo de hash ficava sempre `null`, independentemente do regime da fatura (electronic ou SAF-T).

**Isto significa que todo o sistema de imutabilidade auditado nas rondas 1 a 4 — incluindo a correcção crítica da v1.14.3 — estava a proteger um snapshot sem dados reais.** A protecção contra alteração de campos existia (e funcionava), mas o registo que deveria preservar os valores originais para auditoria e reimpressão fiel estava sempre vazio.

**Correcção:**

1. `InvoiceObserver::created()` deixou de criar o snapshot automaticamente — confirmado que esse momento é sempre prematuro.
2. Novo método público `InvoiceObserver::criarSnapshotAgora(Invoice $invoice)` — chamado explicitamente.
3. `ServicoFatura::criar()` chama `criarSnapshotAgora()` no fim do processo, depois de itens, pagamentos e hash (se aplicável) estarem todos persistidos, com `$fatura->refresh()` antes para garantir que as relações reflectem o estado real da BD.
4. `ServicoFatura::gerarNotaCredito()` corrigido da mesma forma — nunca tinha chamada de criação de snapshot nenhuma até agora.
5. Removida uma chamada `$nc->save()` inconsistente que disparava o guard sem usar `permitirMutacao()` previamente, por segurança e uniformidade com o resto do código.

```
// Antes — sempre vazio, em qualquer regime
Invoice::create([...]); // dispara created() AQUI — sem itens, sem hash
foreach ($itens as $item) { criarLinhaFatura(...); } // itens só agora
gerarEGuardarHashChain($fatura, $orgId); // hash só agora

// Depois — snapshot correcto
Invoice::create([...]);
foreach ($itens as $item) { criarLinhaFatura(...); }
gerarEGuardarHashChain($fatura, $orgId); // ou saveQuietly() em SAF-T
$fatura->refresh();
InvoiceObserver::criarSnapshotAgora($fatura); // SÓ AGORA, com tudo completo
```

### Áreas confirmadas correctas nesta ronda

[](#áreas-confirmadas-correctas-nesta-ronda)

- **Relatórios financeiros** (`resumoFinanceiro`, `resumoIva`, `topClientes`, `maioresDevedores`, `porMeioPagamento`, `receitaPorCategoria`) — todos usam `baseInvoiceQuery()`, que não filtra por regime nem por `agt_status` salvo pedido explícito. Faturas SAF-T são correctamente incluídas nos totais financeiros e fiscais.
- **`RegistoBillingMorph`** e **`ServicoValidacaoPropina`** — independentes do regime de faturação por desenho; sem interacção problemática com `invoicing_mode`.
- **Busca exaustiva de `agt_enabled`** em todo o pacote — todas as ocorrências restantes confirmadas correctas (sincronização de séries com a API AGT só faz sentido em regime electronic).

---

*Onsoft AGT v1.14.4 — Adilson Miguel —  — 2068417074*

---

✅ v1.14.5 — Testes Reais e Comando de Regeneração para a Falha da v1.14.4
-------------------------------------------------------------------------

[](#-v1145--testes-reais-e-comando-de-regeneração-para-a-falha-da-v1144)

Conforme recomendado no final da auditoria anterior, esta versão entrega dois artefactos concretos: testes PHPUnit que provam o comportamento corrigido, e um comando para remediar faturas antigas afectadas pela falha de snapshot vazio.

### Por que não foi possível correr testes neste ambiente

[](#por-que-não-foi-possível-correr-testes-neste-ambiente)

Este ambiente de desenvolvimento não tem PHP instalado — não foi possível executar `vendor/bin/phpunit` directamente aqui. Em vez disso, os testes foram escritos para serem corridos no ambiente real do projecto hospedeiro, e a lógica foi verificada manualmente, linha a linha, seguindo a ordem exacta de execução do Eloquent (confirmando, por exemplo, que `loadMissing()` busca correctamente da BD quando a relação ainda não está em memória).

### `tests/Feature/SnapshotIntegridadeTest.php`

[](#testsfeaturesnapshotintegridadetestphp)

Três casos de teste:

1. **`test_snapshot_contem_itens_e_pagamentos_em_modo_electronic`** — cria uma fatura real com 2 itens e 1 pagamento, e verifica que o `payload_json` do `InvoiceSnapshot` contém ambos os arrays populados, não vazios. Esta é a asserção que teria falhado antes da v1.14.4.
2. **`test_snapshot_contem_itens_em_modo_saft_sem_hash`** — mesmo teste em modo SAF-T, confirmando snapshot completo mas sem `invoice_hash` (correcto para esse regime).
3. **`test_alterar_campo_fiscal_depois_de_criado_e_bloqueado_em_qualquer_regime`** — confirma que `InvoiceSnapshotGuard` bloqueia alteração de `gross_total` em ambos os regimes, validando a correcção da v1.14.3.

**Como correr no projecto real:**

```
composer require --dev orchestra/testbench
vendor/bin/phpunit vendor/productiononschool/onsoft-agt/tests/Feature/SnapshotIntegridadeTest.php
```

### `php artisan onsoft-agt:regenerar-snapshots` — comando de remediação

[](#php-artisan-onsoft-agtregenerar-snapshots--comando-de-remediação)

Detecta faturas com `InvoiceSnapshot.payload_json` tendo `items` ou `payments` vazios — o sintoma exacto da falha pré-v1.14.4 — e oferece regenerá-los.

```
# Apenas detectar, sem alterar nada
php artisan onsoft-agt:regenerar-snapshots --apenas-detectar

# Regenerar com confirmação interactiva
php artisan onsoft-agt:regenerar-snapshots

# Regenerar sem confirmação (scripts/CI)
php artisan onsoft-agt:regenerar-snapshots --forcar
```

**Saída de exemplo:**

```
Encontrados 47 snapshot(s) incompleto(s):

Invoice ID  Documento           Modo        Items vazios  Payments vazios  Risco da regeneração
1024        FR FR-2026/001024   electronic  SIM           SIM              BAIXO — protegida por invoice_hash desde a emissão
1198        FR FR-2026/001198   saft_ao     SIM           SIM              ALTO — sem protecção histórica, dados podem ter mudado

```

### ⚠️ Importante — o que a regeneração NÃO garante

[](#️-importante--o-que-a-regeneração-não-garante)

Regenerar um snapshot usa os dados **actuais** da fatura, não os dados **originais** do momento da emissão — que nunca foram capturados correctamente devido à falha. Para faturas em modo `electronic`, isto é geralmente seguro porque `invoice_hash` protegia os campos fiscais desde sempre. **Para faturas SAF-T criadas antes da v1.14.3, não havia protecção nenhuma** — os dados actuais podem genuinamente já ter divergido dos originais, sem qualquer registo de quando ou como isso aconteceu. O comando assinala isto explicitamente na coluna "Risco" e marca cada snapshot regenerado com `_snapshot_meta.regenerado_em` e `regenerado_motivo`, para que nunca seja confundido com um snapshot original genuíno em auditorias futuras.

### `InvoiceSnapshotGuard::verificarIntegridade()` agora detecta o sintoma automaticamente

[](#invoicesnapshotguardverificarintegridade-agora-detecta-o-sintoma-automaticamente)

`php artisan onsoft-agt:verificar-integridade` passa a reportar faturas com snapshot de itens/pagamentos vazios como não-íntegras, apontando directamente para o comando de regeneração.

---

*Onsoft AGT v1.14.5 — Adilson Miguel —  — 2068417074*

---

🔴 Auditoria Profunda — Sexta Ronda (v1.14.6) — PDF de Fatura Cancelada Não Mostrava Cancelamento
------------------------------------------------------------------------------------------------

[](#-auditoria-profunda--sexta-ronda-v1146--pdf-de-fatura-cancelada-não-mostrava-cancelamento)

Esta ronda confirmou que a correcção da v1.14.4 (ordem de criação do snapshot) está correcta — verificação manual linha a linha confirmou que `loadMissing()` busca correctamente da BD quando a relação ainda não está em memória, e a ordem itens → pagamentos → hash → snapshot está bem sequenciada. Também confirmou que `criarLinhaFatura()` e `BillingPropina` não interferem com essa ordem, e que a verificação de integridade adicionada na v1.14.5 não produz falsos positivos para faturas legitimamente sem pagamentos.

Mas encontrou um problema novo e real, numa área diferente: a interacção entre o snapshot imutável e o estado de cancelamento.

### 🔴 Crítico — PDF gerado do snapshot nunca reflectia cancelamento posterior

[](#-crítico--pdf-gerado-do-snapshot-nunca-reflectia-cancelamento-posterior)

**O problema:** `ServicoPdf::resolverDadosFatura()` e `gerarStreamDeSnapshot()` usavam **exclusivamente** os dados do snapshot quando disponível — incluindo `payment_status` e `agt_status`. Como o snapshot é criado **uma vez**, no momento da emissão, e nunca é actualizado depois (correctamente, por desenho de imutabilidade), esses campos ficavam congelados no estado "não cancelado".

**Consequência real:** o fluxo normal de uso é emitir → snapshot criado → mais tarde, cancelar a fatura. Depois do cancelamento, gerar o PDF (`GET /onsoft-agt/faturas/{id}/pdf`, o endpoint normal) usava o snapshot, e os templates Blade (que decidem mostrar o banner "⛔ DOCUMENTO CANCELADO" com base em `payment_status`) **nunca viam o cancelamento** — porque liam o `payment_status` congelado do momento da emissão, não o estado actual.

Isto significava que **reimprimir uma fatura cancelada produzia um PDF que parecia perfeitamente válido**, sem qualquer indicação de que o documento tinha sido anulado — um risco real de uso indevido, não apenas uma inconsistência cosmética.

**Correcção — separação clara entre dados fiscais e dados de estado:**

```
// Valores FISCAIS continuam fiéis ao momento da emissão (do snapshot):
// itens, totais, hash, assinaturas — exactamente como a imutabilidade exige.

// Campos de ESTADO são sempre sobrepostos com os valores LIVE actuais:
$dados['invoice']['payment_status']  = $fatura->payment_status;   // actual
$dados['invoice']['agt_status']      = $fatura->agt_status;        // actual
$dados['invoice']['cancel_reason']   = $fatura->cancel_reason;     // actual
$dados['invoice']['cancelled_at']    = $fatura->cancelled_at;      // actual
```

Esta correcção foi aplicada em **ambos** os caminhos de leitura do snapshot: `resolverDadosFatura()` (usado por `gerarStream()` e `gerarBase64()`) e `gerarStreamDeSnapshot()` — mesmo este último, desenhado para "reimpressão fiel ao original", agora mostra sempre o estado de cancelamento actual. Foi uma decisão deliberada: preservar fielmente os *valores* fiscais nunca deve significar esconder que o *documento* já não tem validade fiscal.

### Princípio confirmado nesta ronda

[](#princípio-confirmado-nesta-ronda)

A imutabilidade fiscal (Anexo I, ponto 12.l) protege **valores** — montantes, NIF, datas de emissão, hash. Nunca se destinou a congelar o **estado de validade** de um documento. Um documento cancelado continua cancelado independentemente de quando o seu PDF for gerado.

---

*Onsoft AGT v1.14.6 — Adilson Miguel —  — 2068417074*

---

🔴 Auditoria Profunda — Sétima Ronda (v1.15.0) — Faltava o Mecanismo que Resolve `pending` → `accepted`/`rejected`
-----------------------------------------------------------------------------------------------------------------

[](#-auditoria-profunda--sétima-ronda-v1150--faltava-o-mecanismo-que-resolve-pending--acceptedrejected)

Seguindo exactamente a recomendação da ronda anterior, esta auditoria focou-se na transição de estado AGT depois da submissão — e encontrou uma lacuna funcional, não apenas de imutabilidade.

### 🔴 Crítico — `consultarEstado()` existia mas nunca era chamado, e nunca actualizava a fatura

[](#-crítico--consultarestado-existia-mas-nunca-era-chamado-e-nunca-actualizava-a-fatura)

**O problema, em dois níveis:**

1. **`ServicoSubmissao::consultarEstado()`** — o método responsável por perguntar à AGT se uma fatura foi aceite ou rejeitada — actualizava apenas o registo `AgtInvoiceSubmission`. **Nunca tocava no campo `agt_status` da própria `Invoice`.** Mesmo que a AGT respondesse "aceite", a fatura continuava com `agt_status = 'pending'` para sempre.
2. **Código morto** — uma busca exaustiva confirmou que `consultarEstado()` nunca era invocado de lado nenhum no pacote. Sem nenhum endpoint HTTP, comando agendado, ou job a chamá-lo, mesmo a actualização parcial (apenas na tabela de submissões) nunca chegava a acontecer em produção.

**Consequência real:** combinado com a correcção da v1.14.6 (que faz o PDF reflectir o `agt_status` actual da fatura), o sistema ficava preso a mostrar "📤 SUBMETIDO — AGUARDA RESPOSTA DA AGT" para sempre — mesmo faturas aceites ou rejeitadas há semanas continuavam a aparecer como pendentes, porque o "estado actual" nunca era de facto actualizado.

**Correcção em três partes:**

1. `ServicoSubmissao::consultarEstado()` agora propaga o resultado real (`accepted`/`rejected`) para a fatura, respeitando `InvoiceSnapshotGuard::permitirMutacao()` — `agt_status` continua um campo mutável, esta alteração não viola a imutabilidade fiscal.
2. Novo comando `php artisan onsoft-agt:consultar-submissoes` — percorre todas as submissões `pending`/`submitted` (multi-tenant, agrupado por organização para reutilizar o contexto AGT) e chama `consultarEstado()` para cada uma.
3. Agendamento automático no scheduler do pacote, a correr **de 5 em 5 minutos**, ao lado do já existente reset de ano fiscal.

```
// OnsoftAgtServiceProvider::boot()
$schedule->command('onsoft-agt:consultar-submissoes', ['--limite' => 100])
    ->everyFiveMinutes()
    ->withoutOverlapping()
    ->runInBackground();
```

### Novo endpoint — confirmação imediata sob pedido

[](#novo-endpoint--confirmação-imediata-sob-pedido)

```
POST /onsoft-agt/faturas/{id}/estado/consultar-agora

```

Para quando o utilizador não quer esperar pelo próximo ciclo do scheduler — útil imediatamente depois de uma submissão manual.

```
{
  "sucesso": true,
  "dados": {
    "fatura_id": 1247,
    "agt_status": "accepted",
    "submissao": { "status": "accepted", "accepted_at": "2026-06-19T15:32:00Z", "...": "..." },
    "ui": { "pode_submeter": false, "...": "..." }
  }
}
```

### Comando manual

[](#comando-manual)

```
php artisan onsoft-agt:consultar-submissoes
php artisan onsoft-agt:consultar-submissoes --organizacaoId=5 --limite=20
```

---

📋 Resumo de Todas as Correcções Críticas Encontradas (Rondas 1-7)
-----------------------------------------------------------------

[](#-resumo-de-todas-as-correcções-críticas-encontradas-rondas-1-7)

RondaVersãoLacuna crítica1v1.13.0`ServicoLimiteDiario` nunca era chamado — licença e limite diário não eram aplicados2v1.14.0Default inseguro `electronic` quando organização sem config — corrigido para `saft_ao`3v1.14.2Job de fila legado contornava toda a protecção SAF-T na auto-submissão4v1.14.3Faturas SAF-T nunca recebiam snapshot — sem protecção de imutabilidade nenhuma5v1.14.4Snapshot criado antes de itens/pagamentos existirem — sempre vazio, em qualquer regime6v1.14.6PDF do snapshot nunca reflectia cancelamento posterior à emissão7v1.15.0Mecanismo de consulta de estado AGT existia mas nunca era chamado — faturas ficavam presas em `pending`---

*Onsoft AGT v1.15.0 — Adilson Miguel —  — 2068417074*

---

🔴 Correcção Urgente (v1.16.0) — `request_id` e Callback AGT, Apontados Directamente
-----------------------------------------------------------------------------------

[](#-correcção-urgente-v1160--request_id-e-callback-agt-apontados-directamente)

Estas duas lacunas foram apontadas directamente, fora da sequência normal de auditoria — e confirmaram-se ambas reais.

### 🔴 Crítico — `obterEstado()` usava sempre o UUID errado

[](#-crítico--obterestado-usava-sempre-o-uuid-errado)

**O problema:** o campo `request_id` em `AgtInvoiceSubmission` era preenchido com o **nosso UUID gerado no cliente** (`Str::uuid()`, criado antes de sequer contactar a AGT), nunca com o `idLote` real devolvido pela AGT na resposta de `registarFactura()`. Como `obterEstado()` consulta a AGT usando `request_id`, **todas as consultas de estado desde sempre enviavam um identificador que a AGT nunca emitiu** — o mecanismo de polling construído na v1.15.0 nunca teria recebido uma resposta válida em produção real.

**Correcção:** `ServicoSubmissao::enviarParaAgt()` agora extrai o `idLote` da resposta real da AGT (tentando várias chaves plausíveis — `idLote`, `id_lote`, `batchId`, `requestId`, `id` — visto que o nome exacto do campo não está confirmado na documentação disponível) e é **esse** valor, não o nosso UUID, que passa a ser gravado em `request_id` e usado em todas as consultas futuras de estado.

```
// Antes
$requestId = $fatura->submission_uuid ?: Str::uuid(); // gerado ANTES de contactar a AGT
// ... 'request_id' => $requestId  // SEMPRE o nosso UUID, nunca o idLote da AGT

// Depois
$idLoteAgt = $resposta['idLote'] ?? $resposta['id_lote'] ?? $resposta['batchId'] ?? ... ?? $requestId;
// ... 'request_id' => $idLoteAgt  // o idLote REAL devolvido pela AGT
```

**⚠️ Honestidade sobre o que não está confirmado:** o nome exacto do campo que a AGT usa na resposta REST de `/registarFactura` para identificar o lote **não está confirmado** nesta auditoria — a especificação disponível cobre o esquema SOAP/ficheiro do Decreto Executivo, não necessariamente o payload REST actual. A correcção tenta as chaves mais plausíveis e regista um aviso explícito (`Log::warning`) sempre que nenhuma é encontrada, para que o problema seja visível em produção em vez de falhar silenciosamente. **Acção recomendada:** confirmar o nome exacto do campo na documentação REST oficial da AGT e ajustar a lista de chaves tentadas.

### Callback/Webhook AGT — mecanismo que não existia, agora implementado como complemento ao polling

[](#callbackwebhook-agt--mecanismo-que-não-existia-agora-implementado-como-complemento-ao-polling)

**O problema:** o pacote dependia exclusivamente de polling (v1.15.0) para descobrir mudanças de estado — com até 5 minutos de latência. Não havia nenhum endpoint para a AGT notificar directamente.

**Implementado:**

```
POST /onsoft-agt/callback/{organizacaoId}

```

- Regista **sempre** o payload recebido em `onsoft_agt_callback_logs`, antes de qualquer processamento — mesmo que a assinatura seja inválida ou o `idLote` não seja reconhecido, para rasto de auditoria completo.
- Valida assinatura via HMAC-SHA256 sobre um segredo configurado por organização (`agt_callback_secret_encrypted`).
- Actualiza `AgtInvoiceSubmission` e propaga `agt_status` para a fatura, exactamente como o polling já fazia.

**⚠️ Honestidade sobre o que não está confirmado:** se a API AGT efectivamente suporta callbacks/webhooks, qual o esquema de assinatura usado, e o formato exacto do payload — **nada disto está confirmado** na documentação disponível nesta auditoria. A implementação é defensiva: tenta um cabeçalho `X-Agt-Signature` com HMAC-SHA256, mas isto é uma suposição razoável baseada em padrões comuns de webhook, não uma confirmação da especificação real da AGT. **O polling (v1.15.0) continua a ser o mecanismo principal e confiável** até que a especificação real do callback seja confirmada e este código seja ajustado em conformidade.

### Nova tabela `onsoft_agt_callback_logs`

[](#nova-tabela-onsoft_agt_callback_logs)

Auditoria completa de qualquer tentativa de callback recebida — `id_lote_agt`, `estado_recebido`, `assinatura_valida`, `payload`, `headers`, `processado`, `mensagem`.

---

*Onsoft AGT v1.16.0 — Adilson Miguel —  — 2068417074*

---

🔴 RECONSTRUÇÃO TOTAL (v2.0.0) — Reconciliação Completa com a Documentação Oficial da AGT
----------------------------------------------------------------------------------------

[](#-reconstrução-total-v200--reconciliação-completa-com-a-documentação-oficial-da-agt)

Esta versão é a mais importante de todo o histórico do pacote. Todas as versões anteriores (v1.0.0 a v1.16.0) foram construídas a partir do **Decreto Executivo AGT** (texto legal/regulamentar) e de inferências razoáveis sobre como uma API REST moderna "deveria" funcionar. Nunca tínhamos lido a **documentação técnica oficial da API REST**, publicada em:

```
https://quiosqueagt.minfin.gov.ao/doc-agt/faturacao-electronica/1/

```

Ao ler essa documentação na íntegra, descobrimos que **partes estruturais inteiras do pacote estavam incorrectas** — não pequenos ajustes, mas conceitos de base como o algoritmo de assinatura, os endpoints, a autenticação, e o vocabulário de estados. Esta versão reconstrói tudo isso do zero.

### ⚠️ Aviso sobre uma secção desta documentação que ficou desactualizada

[](#️-aviso-sobre-uma-secção-desta-documentação-que-ficou-desactualizada)

As secções anteriores deste README (antes desta, datadas de v1.0.0 a v1.16.0) **contêm afirmações incorrectas** sobre o algoritmo de assinatura (diziam "RSA-SHA1", que nunca existiu na API real) e sobre o mecanismo de callback (que implementámos especulativamente, mas a documentação oficial confirma "Disponível nas próximas versões" — não existe ainda). Essas secções são mantidas como registo histórico de auditoria, mas **não devem ser seguidas** — esta secção (v2.0.0) é a fonte de verdade actual.

### O que mudou — resumo por área

[](#o-que-mudou--resumo-por-área)

ÁreaAntes (errado)Agora (conforme documentação oficial)**Autenticação**Inexistente / customizadaHTTP Basic Auth (username:password Base64) em TODOS os pedidos — credenciais obtidas por email a `produtores.dfe.dcrr.agt@minfin.gov.ao`**Hosts**`quiosqueagt-sandbox.minfin.gov.ao` (inventado)Homologação: `sifphml.minfin.gov.ao` · Produção: `sifp.minfin.gov.ao`**Prefixo de path**Genérico`/sigt/fe/v1/{serviço}`**Método HTTP**Misto GET/POST**TODOS os serviços são POST** — incluindo consultas e listagens**Algoritmo de assinatura**RSA-SHA1 sobre string `;`-concatenada**RS256 (RSA+SHA256)**, JWS Compact Serialization, sobre o **objecto JSON completo**, Base64URL sem padding**Identificador de submissão**UUID gerado no cliente**`requestID`** devolvido por `registarFactura` (string até 15 caracteres) — é este valor, nunca o UUID do cliente, que se usa em `obterEstado`**Custódia da chave do contribuinte**Assumida como gerada localmente**Emitida pela AGT**, disponibilizada no portal do contribuinte — nunca gerada localmente**Tipos de documento**6 tipos (incluindo "FS" inventado)**18 tipos reais**: FA, FT, FR, FG, GF, AC, AR, TV, RC, RG, RE, ND, NC, AF, RP, RA, CS, LD**Vocabulário de estado**`accepted`/`rejected`/`pending`/`failed` (inventado)`resultCode` (0/1/2/7/8/9, nível de lote) + `documentStatus` (`V`/`I`, nível de documento) — ver `EstadoValidacaoAgt`**QR Code**String `;`-separada com hash**URL** para o portal de verificação da AGT: `?emissor={nif}&document={documentNo}`**Callback/Webhook**Implementado especulativamente (v1.16.0)**Removido** — documentação confirma "Disponível nas próximas versões". Único mecanismo real: polling via `obterEstado`**Payload de registo**Estrutura simplificada customEnvelope completo com `softwareInfo`, `documents[]`, `lines[]`, `taxes[]`, `documentTotals`, `paymentReceipt`, `withholdingTaxList`### Ficheiros completamente reescritos nesta versão

[](#ficheiros-completamente-reescritos-nesta-versão)

- **`ServicoAssinatura`** — algoritmo RS256/JWS real, com helpers dedicados para cada uma das 3 assinaturas documentadas (`assinarSoftwareInfo`, `assinarDocumento`, `assinarPedido`)
- **`ServicoApiAgt`** — cliente HTTP completo com Basic Auth, hosts reais, todos os 7 serviços (`registarFactura`, `obterEstado`, `solicitarSerie`, `listarSeries`, `consultarFactura`, `listarFacturas`, `validarDocumento`)
- **`ServicoConstrutorPayloadAgt`** *(novo)* — constrói o objecto `document` completo (linhas, impostos, totais, paymentReceipt) exactamente conforme a documentação, substituindo a dependência externa `AgtInvoicePayloadBuilder` cuja estrutura nunca tinha sido verificada
- **`ServicoFatura::gerarEGuardarHashChain`** — gera as duas assinaturas JWS reais em vez do hash chain RSA-SHA1 fictício; conceito de "hash anterior" removido (não existe na API REST)
- **`ServicoSeries`** — `solicitarSerie`/`listarSeries` com os campos reais (`seriesFEResult`, `seriesInfo`, `establishmentNumber`, `seriesContingencyIndicator`)
- **`ServicoQrCode`** — gera URL de verificação + imagem PNG 350×350px conforme especificação exacta
- **`ServicoSubmissao`** — usa `requestID` real, vocabulário `resultCode`/`documentStatus`, com ponte de compatibilidade para o vocabulário interno via `EstadoValidacaoAgt::mapearParaVocabularioInterno()`
- **`Onsoft\Agt\Enums\TipoDocumento`** — 18 tipos reais (era 6, incluindo um inventado)
- **`Onsoft\Agt\Enums\EstadoDocumentoRegisto`** *(novo)* — `N`/`C` (documentStatus do registo)
- **`Onsoft\Agt\Enums\EstadoValidacaoAgt`** *(novo)* — todos os 4 vocabulários de estado documentados

### Novas migrações

[](#novas-migrações)

```
2024_01_01_000009_add_agt_basic_auth_credentials.php
  → agt_basic_auth_username, agt_basic_auth_password_encrypted,
    establishment_number em organization_agt_configs

```

### Configuração — variáveis que mudaram

[](#configuração--variáveis-que-mudaram)

```
# Antes (URLs inventadas)
AGT_AMBIENTE=sandbox  # apontava para quiosqueagt-sandbox.minfin.gov.ao

# Agora (hosts reais, configurados automaticamente por ambiente)
AGT_AMBIENTE=sandbox  # → https://sifphml.minfin.gov.ao
AGT_AMBIENTE=producao # → https://sifp.minfin.gov.ao
AGT_SCHEMA_VERSION=1.2
```

**Credenciais novas a configurar por organização** (painel AGT → Configuração):

- Username e Password de Basic Auth (solicitar por email a `produtores.dfe.dcrr.agt@minfin.gov.ao`)
- `establishment_number` (usar `"SEDE"` em sandbox ou organizações com um único estabelecimento)

### ⚠️ O que permanece como suposição razoável, não confirmação

[](#️-o-que-permanece-como-suposição-razoável-não-confirmação)

Esta reconciliação cobriu exaustivamente a documentação disponível em `https://quiosqueagt.minfin.gov.ao/doc-agt/faturacao-electronica/1/`. Alguns pontos não estão 100% especificados nessa documentação e foram resolvidos com a opção mais segura disponível:

- **Mapeamento `operationType`** por categoria de item (propina→SE, transporte→STP, etc.) — a documentação lista os códigos válidos mas não dá uma tabela de mapeamento por contexto de negócio; o mapeamento actual é uma inferência razoável para o contexto escolar.
- **`taxCode` por percentagem de IVA** — a documentação lista NOR/INT/RED/ISE/OUT mas não especifica os limiares percentuais exactos de cada categoria; o pacote usa NOR como default seguro para qualquer taxa &gt; 0.
- **Esquema de erro no corpo HTTP 422** (`obterEstado`) — a documentação descreve os códigos E95-E98 mas não mostra um exemplo completo do corpo JSON dessas respostas de erro.

Estes pontos estão assinalados explicitamente nos comentários do código correspondente.

---

*Onsoft AGT v2.0.0 — Adilson Miguel —  — 2068417074*

---

🆕 v2.1.0 — Restrição de Âmbito (FT, FR, NC, ND) + Factura Pró-forma ("FP")
--------------------------------------------------------------------------

[](#-v210--restrição-de-âmbito-ft-fr-nc-nd--factura-pró-forma-fp)

### Restrição de âmbito de tipos de documento

[](#restrição-de-âmbito-de-tipos-de-documento)

Este sistema concreto só precisa de **4 dos 18 tipos reais** suportados pela API AGT. Configurável via `.env`:

```
AGT_TIPOS_ACTIVOS=FT,FR,NC,ND
```

`ServicoFatura::criar()` valida o âmbito **antes de qualquer escrita à BD ou chamada à AGT** — qualquer tentativa de criar um documento fora desta lista é rejeitada de imediato:

```
{
  "sucesso": false,
  "mensagem": "Tipo de documento 'TV' não está activo neste sistema. Tipos permitidos: FT, FR, NC, ND. Para Factura Pró-forma, use Onsoft\\Agt\\Servicos\\ServicoFaturaProforma..."
}
```

`ServicoSeries::inicializarSeriesAnoFiscal()` e o reset automático de ano fiscal usam a mesma lista configurada — nunca criam séries para tipos fora do âmbito.

`gerarNotaCredito()` tem uma verificação de coerência adicional: se `NC` for removida do âmbito activo por engano, o sistema avisa explicitamente em vez de deixar faturas FR/FT sem forma de serem corrigidas após submissão.

O enum `Onsoft\Agt\Enums\TipoDocumento` **continua a suportar os 18 tipos reais** — a restrição é apenas de configuração, não do enum. Qualquer organização que precise de mais tipos no futuro só precisa de ajustar `AGT_TIPOS_ACTIVOS`.

---

### Factura Pró-forma ("FP") — nunca persistida, nunca fiscal

[](#factura-pró-forma-fp--nunca-persistida-nunca-fiscal)

**"FP" não existe nos 18 tipos reais da API AGT** — coerente com a prática: uma pró-forma nunca é um documento fiscal. Implementado como módulo **completamente independente** de `ServicoFatura`.

**Garantia de não-persistência, verificada explicitamente:**

- Nenhuma chamada a `::create()`, `::save()`, `DB::table()->insert()` em todo o `ServicoFaturaProforma` ou `ControladorFaturaProforma`
- Nunca referencia `Invoice` ou `InvoiceItem`
- Nunca chama `ServicoLimiteDiario` (não conta para o limite diário de emissão)
- Nunca consome número de série fiscal
- Nunca assina nada — sem hash, sem `jwsDocumentSignature`
- Nunca contacta a API AGT

Tudo acontece **numa única chamada**: calcular os totais a partir dos itens recebidos no pedido, gerar o HTML, devolver o PDF. Quando a resposta HTTP termina, não fica nenhum rasto na base de dados.

### Endpoints

[](#endpoints-1)

```
POST /onsoft-agt/proforma/calcular     → apenas totais (JSON, sem PDF)
POST /onsoft-agt/proforma/pdf          → PDF em stream
POST /onsoft-agt/proforma/pdf-base64   → PDF em base64 (para SPA)

```

**Exemplo de pedido:**

```
{
  "customer_name": "João Silva",
  "customer_nif": "500123456",
  "validade_dias": 15,
  "items": [
    { "description": "Propina — Outubro 2026", "quantity": 1, "unit_price": 45000, "tax_code": "ISE" },
    { "description": "Material Escolar", "quantity": 2, "unit_price": 2500, "tax_percentage": 14 }
  ]
}
```

**Resposta de `/calcular`:**

```
{
  "sucesso": true,
  "dados": {
    "documento": {
      "tipo": "FP",
      "label": "Factura Pró-forma",
      "valido_ate": "2026-07-04",
      "referencia": "PROFORMA-20260619153000"
    },
    "totais": {
      "subtotal": 50000.00,
      "iva": 700.00,
      "total_geral": 50700.00
    }
  }
}
```

### O que o PDF mostra

[](#o-que-o-pdf-mostra)

- Marca de água diagonal "PRO-FORMA"
- Aviso destacado em vermelho: "DOCUMENTO PRÓ-FORMA — NÃO É FACTURA, SEM VALOR FISCAL"
- Data de validade explícita
- Sem QR Code, sem hash, sem número de série — porque nenhum destes existe para um documento que nunca é fiscal
- Rodapé reforçando: "Não constitui factura, recibo, ou qualquer documento fiscal. Não submetido à AGT."

---

*Onsoft AGT v2.1.0 — Adilson Miguel —  — 2068417074*

---

🔴 Auditoria Completa — Continuação (v2.1.2)
-------------------------------------------

[](#-auditoria-completa--continuação-v212)

Continuação da auditoria completa solicitada, cobrindo as áreas que ainda faltavam: `ServicoExclusividadePagamento`, `ServicoValidacaoPropina`, `RegistoBillingMorph`, `ServicoSaftAo`, `composer.json` e `OnsoftAgtInstalarComando`.

### 🔴 Crítico — Pagamento parcial de propina ficava bloqueado permanentemente

[](#-crítico--pagamento-parcial-de-propina-ficava-bloqueado-permanentemente)

**O problema:** `ServicoValidacaoPropina::validarOrdem()` tratava **qualquer** registo `BillingPropina` não cancelado — incluindo `status = 'partial'` — como "já pago" para efeitos de bloqueio de repetição. Isto significava que, uma vez que um mês ficasse parcialmente pago, **nunca mais podia ser pago na totalidade** — qualquer tentativa de criar uma nova fatura para completar o saldo era rejeitada com "já está pago".

**Consequência adicional encontrada:** mesmo corrigindo a validação, `validarECriarPropinas()` sempre fazia `BillingPropina::create()`, nunca `update()` — se a validação permitisse completar um mês parcial, isso criaria um **segundo registo duplicado** para o mesmo mês, em vez de complementar o existente.

**Correcção em duas partes:**

1. `validarOrdem()` agora distingue `'paid'` (bloqueia repetição) de `'partial'`/`'pending'` (ocupa a posição na sequência, mas pode ser completado)
2. `validarECriarPropinas()` agora verifica se já existe um registo não-cancelado para o mês e, se existir, **soma** ao valor existente em vez de duplicar

### Duplicação de lógica com cache divergente — `ServicoFatura::validarExclusividadeMetodos()`

[](#duplicação-de-lógica-com-cache-divergente--servicofaturavalidarexclusividademetodos)

Esta função reimplementava, linha a linha, exactamente a mesma lógica de `ServicoExclusividadePagamento::validar()` — mas com uma **chave de cache diferente** (`onsoft_agt_tipodepagamento_todos` vs `onsoft_agt_tipodepagamento_lista`). Invalidar a cache num serviço nunca invalidava a do outro — risco real de validação com dados desactualizados após alterar `tipodepagamento.exclusivo`. Corrigido: `ServicoFatura` agora delega para `ServicoExclusividadePagamento::validar()`, eliminando a duplicação e o risco de divergência.

### `composer.json` — Facade inexistente registada

[](#composerjson--facade-inexistente-registada)

`extra.laravel.aliases` registava `OnsoftAgt` apontando para `Onsoft\Agt\Fachadas\OnsoftAgt` — uma classe que nunca existiu no pacote. Isto causaria erro real ao instalar com Laravel auto-discovery activo. Removido (nenhum código ou documentação dependia deste alias).

### `RegistoBillingMorph` — documentação divergente da implementação

[](#registobillingmorph--documentação-divergente-da-implementação)

A tabela de documentação listava `categoria_produto` → `PedagogicalProductCategory`, mas esse tipo nunca esteve em `inicializar()`. Corrigido removendo a linha da documentação (facturar uma categoria de produto, em vez do produto em si, não é um caso de uso esperado).

### `ServicoSaftAo` — honestidade sobre o âmbito da reconciliação

[](#servicosaftao--honestidade-sobre-o-âmbito-da-reconciliação)

Adicionado aviso explícito: a reconciliação completa desta auditoria validou a API REST de Faturação Eletrónica contra a documentação oficial. O esquema XML SAF-T(AO) implementado segue a estrutura genérica OECD/SAF-T, mas **não foi confirmado** com a mesma certeza contra um XSD oficial AGT específico para SAF-T(AO) — esse documento não estava disponível no portal consultado.

### Confirmado correcto sem alterações

[](#confirmado-correcto-sem-alterações)

`OnsoftAgtInstalarComando`, tags de `publishes()`, `mergeConfigFrom()`, todas as dependências declaradas em `composer.json` (Guzzle, DomPDF, BaconQrCode) confirmadas em uso real.

---

*Onsoft AGT v2.1.2 — Adilson Miguel —  — 2068417074*

---

🔴 Lacuna Crítica Encontrada — Fluxo de Correcção de Fatura Rejeitada (v2.2.0)
-----------------------------------------------------------------------------

[](#-lacuna-crítica-encontrada--fluxo-de-correcção-de-fatura-rejeitada-v220)

Em resposta directa à pergunta "falta alguma coisa para o pacote estar 100% alinhado com a AGT", uma verificação adicional às áreas ainda não auditadas com rigor (`ServicoFlagsUiFatura`) encontrou uma lacuna real e significativa.

### O problema

[](#o-problema)

A documentação oficial da AGT (Registar Factura, regra FE-RNG-073, erro E46) é explícita:

> *"A emissão de documentos com o mesmo número de identificação no campo documentNo de outro documento previamente enviado e rejeitado pela AGT não é aceite. As correcções de documentos rejeitados deverão ser efectuados com a utilização de um novo número de documento."*

Apesar disto, `ServicoFlagsUiFatura` mostrava o botão "Submeter à AGT" também para faturas com `agt_status = 'rejected'` — levando o utilizador a tentar resubmeter exactamente o mesmo documento, que a AGT rejeitaria outra vez, **indefinidamente, sem nenhum caminho de saída**.

Pior: `ServicoConstrutorPayloadAgt` já lia um campo `$fatura->rejectedDocumentNo` (para preencher correctamente `documentStatus = 'C'` e `rejectedDocumentNo` no payload) — mas essa coluna **nunca existiu** na base de dados, e nenhum fluxo a preenchia. O suporte estava parcialmente construído, mas inatingível.

### A correcção

[](#a-correcção-1)

1. **Nova migração** `2024_01_01_000010_add_rejected_document_no.php` — adiciona a coluna real `rejected_document_no` (nome corrigido de `rejectedDocumentNo` camelCase inexistente para o padrão snake\_case real do schema).
2. **`ServicoConstrutorPayloadAgt`** corrigido para ler o nome de coluna correcto.
3. **Novo método `ServicoFatura::corrigirFaturaRejeitada()`** — cria uma nova fatura com novo `documentNo`, copiando itens e pagamentos da rejeitada, preenchendo `rejected_document_no` com o documento original. A fatura rejeitada original nunca é apagada ou alterada — fica como registo histórico permanente.
4. **`ServicoFlagsUiFatura`** corrigido — `'rejected'` removido da lista de estados que mostram "Submeter"; nova flag `mostrar_botao_corrigir_rejeitada` para o único caminho válido.
5. **Novo endpoint** `POST /onsoft-agt/faturas/{id}/corrigir-rejeitada`.

```
{
  "alteracoes": {
    "customer_nif": "500999888"
  }
}
```

```
{
  "sucesso": true,
  "mensagem": "Nova fatura FR FR-2026/001250 criada, referenciando a rejeitada FR FR-2026/001247.",
  "dados": { "...": "..." }
}
```

### Confirmação de consistência

[](#confirmação-de-consistência)

`ServicoFaturasAluno::formatarFaturaParaAluno()` já tinha a regra correcta (`pode_submeter` sem `'rejected'`) — confirma que esta era uma inconsistência isolada em `ServicoFlagsUiFatura`, não um padrão repetido em todo o pacote.

---

*Onsoft AGT v2.2.0 — Adilson Miguel —  — 2068417074*

---

🔴 Correcção Crítica — Violação PSR-4 em Excecoes.php (v2.2.1)
-------------------------------------------------------------

[](#-correcção-crítica--violação-psr-4-em-excecoesphp-v221)

### O problema, reportado directamente em produção

[](#o-problema-reportado-directamente-em-produção)

Ao instalar o pacote num projecto real e correr `composer dump-autoload -o` (autoload optimizado), o seguinte erro ocorria ao usar qualquer excepção do pacote excepto `ExcecaoFaturaAgt`:

```
Error: Class "Onsoft\Agt\Excecoes\ExcecaoConfiguracaoAgt" not found.

```

### Causa raiz

[](#causa-raiz)

`src/Excecoes/Excecoes.php` continha **8 classes** num único ficheiro: `ExcecaoOnsoftAgt`, `ExcecaoApiAgt`, `ExcecaoAutenticacaoAgt`, `ExcecaoAssinaturaAgt`, `ExcecaoFaturaAgt`, `ExcecaoSerieAgt`, `ExcecaoConfiguracaoAgt`, `ExcecaoPdfAgt` — todas num ficheiro chamado `Excecoes.php`, que não corresponde ao nome de nenhuma delas.

Isto **viola a norma PSR-4**, que exige um ficheiro por classe, com o nome do ficheiro a corresponder exactamente ao nome da classe. O autoload **dinâmico** do Composer (sem `-o`) tolera isto na maioria dos casos, porque faz scan de fallback quando uma classe não é encontrada no mapa inicial. O autoload **optimizado** (`-o`, usado em produção e recomendado em qualquer ambiente real) gera um classmap estático a partir de inferência de ficheiro→classe e **não faz esse fallback** — o resultado é que apenas algumas das 8 classes ficavam registadas, de forma imprevisível, e qualquer tentativa de usar as restantes falhava com "Class not found", mesmo com o ficheiro fisicamente presente e sintacticamente correcto no disco.

### Correcção

[](#correcção)

Cada uma das 8 classes foi movida para o seu próprio ficheiro, com o nome exacto da classe:

```
src/Excecoes/ExcecaoOnsoftAgt.php
src/Excecoes/ExcecaoApiAgt.php
src/Excecoes/ExcecaoAutenticacaoAgt.php
src/Excecoes/ExcecaoAssinaturaAgt.php
src/Excecoes/ExcecaoFaturaAgt.php
src/Excecoes/ExcecaoSerieAgt.php
src/Excecoes/ExcecaoConfiguracaoAgt.php
src/Excecoes/ExcecaoPdfAgt.php

```

`Excecoes.php` foi removido. Nenhuma classe foi renomeada — todos os `use Onsoft\Agt\Excecoes\...` existentes no resto do pacote continuam a funcionar sem qualquer alteração, porque os namespaces e nomes de classe são idênticos; só a organização física em disco mudou.

### Auditoria de confirmação

[](#auditoria-de-confirmação)

Verificação exaustiva a todo o pacote confirmou que `Excecoes.php` era o **único** ficheiro com esta violação — os restantes 50 ficheiros do pacote já seguiam correctamente uma classe por ficheiro.

### Acção necessária se já tem o pacote instalado

[](#acção-necessária-se-já-tem-o-pacote-instalado)

```
composer update productiononschool/onsoft-agt
composer dump-autoload -o
```

---

*Onsoft AGT v2.2.1 — Adilson Miguel —  — 2068417074*

---

🆕 v2.2.2 — Comando para Gerar a Chave do Software
-------------------------------------------------

[](#-v222--comando-para-gerar-a-chave-do-software)

Antes desta versão, gerar a chave do software exigia abrir `php artisan tinker` e colar manualmente um bloco de código — sujeito a erros de copy/paste com heredocs e escaping. Novo comando dedicado:

```
php artisan onsoft-agt:gerar-chave-software
```

Gera o par de chaves RSA (2048 bits por defeito) directamente em `storage/app/agt/software_privada.pem` e `software_publica.pem`, com permissões restritivas (`chmod 600`) na chave privada.

### Opções

[](#opções)

```
php artisan onsoft-agt:gerar-chave-software --bits=4096
php artisan onsoft-agt:gerar-chave-software --forcar          # substitui sem perguntar
php artisan onsoft-agt:gerar-chave-software --mostrar-env      # imprime já no formato .env
```

### Saída

[](#saída)

```
🔑 Gerando par de chaves RSA de 2048 bits...
✅ Chaves geradas e guardadas:
   Privada: /caminho/storage/app/agt/software_privada.pem
   Pública: /caminho/storage/app/agt/software_publica.pem

⚠️  Esta é a chave do SOFTWARE — gerada localmente por ti.
    A chave do CONTRIBUINTE é diferente: é emitida pela AGT e
    obtida no portal do contribuinte — este comando NÃO a gera.

Próximos passos:
  1. Copie a chave PÚBLICA (acima) para o Portal do Parceiro AGT...
  2. A AGT devolve um número de certificação (ex: C_134).
  3. Configure o .env com os valores abaixo.

# ── Cole estas linhas no seu .env ──────────────────────────
AGT_SOFTWARE_CHAVE_PRIVADA="-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
AGT_SOFTWARE_CHAVE_PUBLICA="-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
AGT_SOFTWARE_NUMERO_CERTIFICACAO=  # preencher depois do registo no Portal do Parceiro

```

A opção `--mostrar-env` já converte automaticamente as quebras de linha reais do PEM para `\n` literal, no formato exacto que o `.env` exige — elimina a fonte de erro mais comum ao configurar isto manualmente.

### O que este comando NÃO faz

[](#o-que-este-comando-não-faz)

Não gera a chave do **contribuinte** — essa é emitida pela AGT e obtida no portal do contribuinte da organização, nunca gerada localmente. O comando avisa isto explicitamente na sua própria saída, para não haver confusão entre as duas chaves.

---

*Onsoft AGT v2.2.2 — Adilson Miguel —  — 2068417074*

###  Health Score

20

—

LowBetter than 13% of packages

Maintenance65

Regular maintenance activity

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity11

Early-stage or recently created project

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/70519246?v=4)[Adilson Jose Miguel](/maintainers/onsoftAdilsonjose)[@onsoftAdilsonjose](https://github.com/onsoftAdilsonjose)

---

Top Contributors

[![productionOnschool](https://avatars.githubusercontent.com/u/156353041?v=4)](https://github.com/productionOnschool "productionOnschool (9 commits)")

### Embed Badge

![Health badge](/badges/productiononschool-onsoft-agt/health.svg)

```
[![Health](https://phpackages.com/badges/productiononschool-onsoft-agt/health.svg)](https://phpackages.com/packages/productiononschool-onsoft-agt)
```

###  Alternatives

[omnipay/coinbase

Coinbase driver for the Omnipay payment processing library

18570.2k1](/packages/omnipay-coinbase)[oxid-esales/amazon-pay-module

AmazonPay module for OXID

1824.3k](/packages/oxid-esales-amazon-pay-module)[yenepay/php-sdk

YenePay SDK for PHP

112.7k](/packages/yenepay-php-sdk)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
