# Integração Moloni e núcleo multi-ERP — plano

Data: 2026-07-22 · Estado: aprovado (arquitetura), por executar
Decisão do utilizador: **núcleo neutro + refactor Cegid** (em vez de silo Moloni
paralelo ou híbrido só de interfaces).

## Contexto

- O Cegid está implementado como silo: entidades `Cegid*`
  (`CegidCommit`, `CegidOperation`, `CegidSalesDocument`, bindings, sync runs),
  serviços `Cegid*` e o `IntegrateDocumentJobHandler` acoplado ao
  `ICegidApiClient`. Os job types `SyncCegidSales` e `ReconcileCegid` são
  específicos do provider.
- A Fase 1 do plano `company-integration-provider-selection-plan.md` já está
  feita: `CompanyIntegrationSettings.Provider` (`None`/`Cegid`/`Moloni`) por
  empresa interna, com UI condicional. `Moloni` é hoje apenas uma opção sem
  integração funcional.
- Vêm aí mais integrações (PHC, Primavera, Artsoft). Sem dados de produção a
  preservar, o rename das tabelas Cegid para um núcleo neutro é de baixo risco
  e evita duplicar o motor de estados/reconciliação por cada ERP.

## Resumo da API Moloni (o que interessa ao Sibyla)

Fonte: https://www.moloni.pt/dev/ (endpoints, autenticação, utilização).

- **Transporte**: tudo `POST https://api.moloni.pt/v1/{classe}/{ação}/` com
  `?access_token=...&json=true&human_errors=true`; body em JSON. Erros vêm com
  HTTP 400 genérico + objeto `error`/`error_description`.
- **Autenticação**: OAuth 2.0. Fluxo web (recomendado pela Moloni): redirect
  para `https://www.moloni.pt/ac/root/oauth/` com `client_id` (Developer ID) e
  `redirect_uri`; troca do `code` por tokens em `/v1/grant/`. Alternativa
  `grant_type=password` (aplicações nativas) exigiria guardar credenciais do
  utilizador — **rejeitada**.
- **Validade dos tokens**: access token **1 hora**; refresh token **14 dias**.
  O refresh devolve sempre um par novo. Se o refresh expirar, é preciso
  reautorização humana.
- **Âmbito**: quase todas as chamadas exigem `company_id` (obtido via
  `companies/getAll`). Não existe o conceito Cegid de subentidade por ano
  fiscal.
- **Compras (push)**: `suppliers` (getByVat/insert/update), `products`
  (getByReference/insert), `supplierInvoices`, `supplierCreditNotes`,
  `supplierDebitNotes`, `supplierReceipts` (pagamentos a fornecedores).
- **Vendas (pull)**: `invoices`, `invoiceReceipts`, `receipts`, `creditNotes`,
  `debitNotes`, `simplifiedInvoices` — `getAll` com paginação `offset`/`qty`;
  PDF via `documents/getPDFLink`.
- **Dados de suporte a mapear**: `taxes` (taxas/isenções IVA),
  `documentSets` (séries), `paymentMethods`, `maturityDates`,
  `measurementUnits`, `warehouses`, `taxExemptions` (motivos de isenção).
- **Sem idempotência nativa**: os `insert` não aceitam chave de idempotência —
  o nosso ledger de operações + reconciliação por pesquisa é obrigatório.
- **Sandbox** disponível para desenvolvimento/testes.
- **Limitações conhecidas**: sem endpoint público de upload de anexos a
  documentos (confirmar com o suporte Moloni); rate limits não documentados
  publicamente.

## Fase A — Núcleo neutro (refactor Cegid)

Objetivo: ledger e orquestração provider-agnósticos; Cegid passa a ser o
primeiro adaptador. Comportamento Cegid observável inalterado.

1. **Renomear entidades/tabelas do ledger** (migração EF de rename, sem perda —
   não há dados de produção):
   - Nota de deploy: a migração `20260723013008_NeutralIntegrationLedger` é
     destrutiva por desenho para as tabelas partilhadas `Cegid*` e deve ser
     aplicada apenas depois de comprovar, no ambiente alvo, que
     `CegidCommits`, `CegidOperations`, bindings, sales e sync runs estão
     vazias. A própria migração falha fechada se encontrar dados Cegid nesses
     ledgers; o rollback também falha se já existirem dados `Integration*` ou
     IDs externos não reversíveis para `bigint`.
   - `CegidCommit` → `IntegrationCommit` (+ coluna `Provider`)
   - `CegidOperation` → `IntegrationOperation`
   - `CegidSalesDocument`/`Line`/`Receipt`/`ReceiptAllocation`/`PdfAsset` →
     `IntegrationSales*` (+ `Provider`)
   - `CegidProductBinding`/`CegidPartyBinding` → `IntegrationProductBinding`/
     `IntegrationPartyBinding` (+ `Provider`)
   - `CegidSyncRun` → `IntegrationSyncRun`; `CegidProductPlKeyMapping` →
     `IntegrationProductPlKeyMapping`
   - Enums `CegidCommitState`, `CegidOperationKind`, etc. → `Integration*`
     (valores já são genéricos).
2. **IDs externos como `string`**: os campos `long CegidProductId`,
   `CegidPartyId`, `CegidDocumentId`, `CegidReceiptId`, `CegidPayableId`, etc.
   passam a `string ExternalId` — Moloni usa inteiros mas PHC/Primavera/Artsoft
   usam chaves alfanuméricas. Conversão no adaptador.
3. **`FiscalYear`/`SubentityId`**: manter `FiscalYear` como coluna neutra
   opcional (Cegid exige, Moloni ignora); `CegidFiscalYearMapping` permanece
   específico do Cegid na tabela de configuração.
4. **Job types neutros**: `SyncCegidSales` → `SyncProviderSales`,
   `ReconcileCegid` → `ReconcileIntegration`; payloads passam a resolver o
   provider pela empresa (`CompanyIntegrationSettings`). `IntegrateDocument` já
   tem nome neutro.
5. **Interfaces de provider** (em `Sibyla.Domain` ou
   `Sibyla.Infrastructure/Integrations`):
   - `IIntegrationConnectionTester` — teste de ligação/configuração
   - `IIntegrationPurchaseWriter` — ensure supplier/product, escrever documento
     de compra, anexos (capacidade opcional), pagamento
   - `IIntegrationSalesReader` — pull de documentos/recibos/PDFs de venda
   - `IIntegrationReconciler` — resolução de `WriteUnknown`
   - Registo por provider + **dispatcher** que resolve o adaptador a partir de
     `CompanyIntegrationSettings.Provider`; expor capacidades
     (`SupportsAttachments`, `RequiresFiscalYear`, ...) para a UI e validações.
6. **Refactor dos serviços**: `IntegrateDocumentJobHandler`,
   `CegidSalesSyncService`, `CegidReconciliationService`,
   `FinancialWorkbookProjector` e o hosted service de polling passam a operar
   sobre o ledger neutro e a delegar chamadas ao adaptador; a lógica HTTP
   Cegid fica isolada em `Infrastructure/Integrations/Cegid/`.
7. **Configuração**: `CegidCompanyConnection` mantém-se específica (cada
   provider terá a sua tabela de ligação). Generalizar o protetor de segredos
   (`ICegidSecretProtector` → `IIntegrationSecretProtector`, mesmo certificado
   e application name para não invalidar segredos existentes).
8. **API/UI**: mover endpoints genéricos (commits, operações, reconciliação)
   para rotas neutras `/api/integrations/...`, mantendo rotas de configuração
   por provider; atualizar `database-erd.md` e `cegid-integration.md`.
9. **Aceitação**: build sem warnings; suite de testes completa verde; fluxo
   Cegid de ponta a ponta (compras push + vendas pull) sem alteração de
   comportamento.

## Fase B — Ligação Moloni (configuração + OAuth + UI)

1. **`MoloniCompanyConnection`** (padrão de `CegidCompanyConnection`):
   `DeveloperId`, `ProtectedClientSecret`, `RedirectUri`, `MoloniCompanyId`,
   `ProtectedRefreshToken`, `TokenIssuedAt`/`RefreshTokenExpiresAt`,
   `DocumentSetId` (série por omissão), `PaymentMethodId`, `MaturityDateId`,
   `WarehouseId`, `SalesBackfillDays`, versionamento/teste de ligação
   (`ConfigurationVersion`, `LastTestSucceeded`, ...), auditoria.
2. **Fluxo OAuth web**: endpoint de callback em `Sibyla.Api`
   (`/api/integrations/moloni/oauth/callback`) + arranque da autorização a
   partir da UI; guardar refresh token protegido (DPAPI/certificado, como o
   Cegid). Nunca persistir passwords Moloni.
3. **`MoloniAccessTokenProvider`**: cache do access token (~1 h), refresh
   proativo; **job de manutenção periódico** que renova o refresh token muito
   antes dos 14 dias. Se expirar, marcar a ligação como
   "reautorização necessária", alertar o operador e falhar fechado.
4. **Seleção da empresa Moloni**: após autorização, listar `companies/getAll`
   e fixar `company_id`; validar NIF da empresa Moloni contra a empresa interna.
5. **UI `Companies.razor`**: com `Provider = Moloni`, substituir a nota
   "integração ainda não implementada" pela secção de configuração Moloni
   (botão de autorizar, teste de ligação, defaults). Remover o tratamento
   especial só-Cegid em `CompanyIntegrationSettingsService` (guarda de mudança
   de provider passa a genérica: bloquear mudança quando existir ligação
   configurada/atividade do provider atual).
6. **Teste de ligação**: `companies/getOne` + carregamento de taxas/séries como
   validação de configuração (equivalente ao `CegidConnectionTester`).

## Fase C — Compras Moloni (push)

1. **Mapeamentos prévios por empresa** (cache local, refrescável):
   taxas IVA (`taxes/getAll`, por percentagem/região fiscal; isenções exigem
   `exemption_reason` válido), série (`documentSets`), métodos de pagamento,
   unidades de medida, armazém.
2. **Fluxo por documento aprovado** (operações no ledger neutro, mesma máquina
   de estados Prepared → Writing → Confirmed/WriteUnknown/Failed):
   - ensure supplier: `suppliers/getByVat` → `insert` se ausente; gravar
     `IntegrationPartyBinding`
   - ensure products: `products/getByReference` → `insert`; gravar
     `IntegrationProductBinding`
   - documento: `supplierInvoices/insert` (ou `supplierCreditNotes`/
     `supplierDebitNotes` conforme o tipo), com `number` = número fiscal do
     fornecedor, datas, linhas com `tax_id` mapeado; **decidir** inserir como
     rascunho vs fechado — recomendação: rascunho no piloto, fechado quando o
     fluxo estiver validado
   - pagamento aprovado: `supplierReceipts/insert` com alocações (paralelo do
     `PurchasePaymentInstruction` atual)
3. **Idempotência/reconciliação**: registar a operação antes do write; após
   timeout/ambiguidade, pesquisar por fornecedor + `number` + data + total
   antes de repetir (Moloni não deduplica).
4. **Anexos**: sem upload via API — o PDF original permanece no arquivo
   Sibyla/Nextcloud; registar a limitação por capacidade do provider
   (`SupportsAttachments = false`) e confirmar com o suporte Moloni.
5. **Casos a mapear**: moeda estrangeira (`exchange_rate`), retenções
   (`deductions`), IVA de autoliquidação/isenções com motivo obrigatório.

## Fase D — Vendas Moloni (pull)

1. `SyncProviderSales` para Moloni: `invoices`, `invoiceReceipts`,
   `creditNotes`, `debitNotes`, `simplifiedInvoices`, `receipts` — `getAll`
   paginado (`offset`/`qty`), janela por `SalesBackfillDays`; detetar alterações
   por hash do payload (padrão atual do Cegid); estados anulados refletidos.
2. PDFs: `documents/getPDFLink` → download → storage
   (`IntegrationSalesPdfAsset`).
3. Recibos/alocações a documentos e projeção para o workbook via o projector
   neutro (Fase A), incluindo `IntegrationProductPlKeyMapping`.
4. Polling: hosted service único que despacha por provider/empresa ativa.

## Fase E — Reconciliação, testes e documentação

1. `IIntegrationReconciler` Moloni: resolver `WriteUnknown` por pesquisa e
   confirmar/falhar operações; job `ReconcileIntegration` cobre ambos os
   providers.
2. Testes: unitários (mapeamento de taxas, token provider, payloads) e
   integração contra o **sandbox Moloni**; regressão Cegid completa.
3. Retry/backoff e tratamento de 400 com `error_description`; monitorizar
   rate limiting empiricamente.
4. Documentação: `docs/moloni-integration.md` operacional (padrão de
   `cegid-integration.md`), atualização de `database-erd.md`, README e
   runbooks (reautorização OAuth é procedimento de operação).

## Riscos e questões em aberto

- **Refresh token de 14 dias**: paragem do Worker superior a 14 dias obriga a
  reautorização humana — alerta obrigatório e procedimento no runbook.
- **Sem idempotência nativa nos inserts** — a reconciliação por pesquisa é o
  único mecanismo; testar agressivamente timeouts/duplicados.
- **Anexos indisponíveis via API** — validar com o suporte Moloni; caso mude,
  ativar a capacidade no adaptador.
- **Rate limits não documentados** — medir no sandbox.
- Confirmar que o plano/subscrição Moloni do cliente inclui acesso à API e
  criar a conta de developer (Developer ID, Redirect URI, Client Secret).
- Rascunho vs fechado no `supplierInvoices/insert` — decidir no piloto.

## Preparação para PHC / Primavera / Artsoft

Depois das Fases A–B, adicionar um ERP passa a ser: novo valor no enum
`IntegrationProvider`, nova tabela de ligação específica, novo adaptador que
implementa as interfaces e o seu registo no dispatcher, mais a secção de UI.
O ledger, a máquina de estados, os jobs, a reconciliação, o projector e a UI
genérica não se alteram.
