Laravel Moloni
Integracao completa com a API de faturacao Moloni para Laravel. Gestao de clientes, fornecedores, produtos, faturas e todos os documentos fiscais com type safety total. v2.1 adiciona 12 novos recursos de documentos (Documentos Internos, Devolucoes, Guias Globais, ciclo de fornecedor completo) e suporte oficial a sandbox.
Overview
O Laravel Moloni e um pacote que integra a sua aplicacao Laravel com a API do Moloni, a plataforma de faturacao mais utilizada em Portugal. Oferece uma interface fluente e tipada para gerir empresas, clientes, fornecedores, produtos, faturas e todos os outros recursos Moloni, com type safety total atraves de DTOs e enums.
Tipos de documentos suportados
Cobertura completa do catalogo Moloni: ciclo de venda, ciclo de compra, devolucoes e guias.
Venda
- Faturas
- Fatura-Recibos
- Faturas Simplificadas
- Recibos
- Notas de Credito
- Notas de Debito
- Orcamentos
- Devolucao de Pagamentos
- Devolucoes de Cliente
Compra v2.1
- Faturas de Fornecedor
- Faturas Simplificadas Fornecedor
- Ordens de Compra a Fornecedor
- Notas de Credito Fornecedor
- Notas de Debito Fornecedor
- Devolucoes a Fornecedor
- Recibos de Fornecedor
- Pedidos de Garantia
Guias & outros
- Guias de Remessa
- Guias de Transporte
- Cartas de Porte
- Guias Globais
- Guias de Movimento de Activos
- Documentos Internos
- Ordens de Compra
- Documentos Genericos
Instalacao
Instale o pacote via Composer, publique a configuracao e execute as migrations.
composer require digitaldev-lx/laravel-moloni
php artisan vendor:publish --tag=moloni-config
php artisan migrate
Configuracao
Adicione as credenciais da API Moloni ao ficheiro .env. As credenciais sao obtidas no Moloni Developer Portal.
MOLONI_CLIENT_ID=your-client-id
MOLONI_CLIENT_SECRET=your-client-secret
MOLONI_USERNAME=your-username
MOLONI_PASSWORD=your-password
MOLONI_COMPANY_ID=your-company-id
# Opcional. Default: producao. Sobrepor para apontar a sandbox.
# Producao: https://api.moloni.pt/v1/
# Sandbox: https://api.moloni.pt/sandbox/
MOLONI_BASE_URL=https://api.moloni.pt/v1/
Variaveis de ambiente
| Variavel | Descricao |
|---|---|
| MOLONI_CLIENT_ID | Client ID da aplicacao registada no Moloni |
| MOLONI_CLIENT_SECRET | Client Secret da aplicacao |
| MOLONI_USERNAME | Email da conta Moloni |
| MOLONI_PASSWORD | Password da conta Moloni |
| MOLONI_COMPANY_ID | ID da empresa no Moloni |
| MOLONI_BASE_URL | Base URL da API (opcional). Util para sandbox ou ambientes de teste. Default: https://api.moloni.pt/v1/ |
Sandbox
O Moloni disponibiliza um ambiente de sandbox para testar integracoes sem afectar dados de producao. Expoe os mesmos endpoints que a API real, em https://api.moloni.pt/sandbox/.
Obter credenciais
- Precisas de uma conta Moloni activa. Se o teu login ainda nao tem acesso a empresa de demonstracao, pede a
apoio@moloni.ptindicando o email associado. - O
client_id/client_secretOAuth2 sao os mesmos que producao. Apenas abase_urlmuda. - Podes explorar a API interactivamente no Moloni API Explorer.
Alternar entre ambientes
# Sandbox
MOLONI_BASE_URL=https://api.moloni.pt/sandbox/
# Producao (ou omitir a variavel)
MOLONI_BASE_URL=https://api.moloni.pt/v1/
Persistencia de tokens entre ambientes
Os tokens OAuth2 sao guardados na tabela moloni_tokens e nao sao isolados por ambiente. Ao mudar MOLONI_BASE_URL, o token existente deixa de ser valido no novo host e o pacote re-autentica-se silenciosamente na proxima chamada. Para forcar um estado limpo, trunca a tabela:
php artisan tinker --execute="\DigitaldevLx\LaravelMoloni\Models\MoloniToken::truncate();"
Padrao recomendado: usar bases de dados separadas para local / testing (ja e o default com SQLite) para que tokens de producao nunca sejam reutilizados contra o host de sandbox.
Clientes
Gerir clientes atraves da facade Moloni. Pode listar, pesquisar por NIF e criar novos clientes usando DTOs tipados ou arrays simples.
use DigitaldevLx\LaravelMoloni\Facades\Moloni;
$companyId = config('moloni.company_id');
// Listar todos os clientes
$customers = Moloni::customers()->getAll($companyId);
// Pesquisar por NIF
$customer = Moloni::customers()->getByVat($companyId, '123456789');
use DigitaldevLx\LaravelMoloni\DataTransferObjects\Customer as CustomerDto;
$dto = new CustomerDto(
vat: '123456789',
number: 'C001',
name: 'John Doe',
email: 'john@example.com',
address: '123 Main Street',
city: 'Lisbon',
zipCode: '1000-001',
countryId: 1,
);
$customer = Moloni::customers()->insert($companyId, $dto);
$customer = Moloni::customers()->insert($companyId, [
'vat' => '123456789',
'number' => 'C001',
'name' => 'John Doe',
'email' => 'john@example.com',
]);
Produtos
Gestao completa do catalogo de produtos com suporte a categorias, tipos e unidades de medida.
$companyId = config('moloni.company_id');
// Listar todos
$products = Moloni::products()->getAll($companyId);
// Pesquisar por referencia
$product = Moloni::products()->getByReference($companyId, 'PROD-001');
use DigitaldevLx\LaravelMoloni\DataTransferObjects\Product as ProductDto;
use DigitaldevLx\LaravelMoloni\Enums\ProductType;
$dto = new ProductDto(
name: 'Widget',
reference: 'PROD-001',
type: ProductType::Product,
categoryId: 1,
unitId: 1,
price: 29.99,
);
$product = Moloni::products()->insert($companyId, $dto);
Documentos e Faturas
Emita faturas e outros documentos fiscais com DTOs tipados para produtos, pagamentos e datas. O pacote suporta todos os tipos de documentos do Moloni.
use DigitaldevLx\LaravelMoloni\DataTransferObjects\Document as DocumentDto;
use DigitaldevLx\LaravelMoloni\DataTransferObjects\DocumentProduct;
use DigitaldevLx\LaravelMoloni\DataTransferObjects\Payment;
$dto = new DocumentDto(
documentSetId: 1,
customerId: 1,
date: '2026-03-31',
expirationDate: '2026-04-30',
products: [
new DocumentProduct(
productId: 1,
qty: 2,
price: 29.99,
),
],
payments: [
new Payment(
paymentMethodId: 1,
value: 59.98,
date: '2026-03-31',
),
],
);
$invoice = Moloni::invoices()->insert($companyId, $dto);
// A partir de v2.0, getPdfLink() usa o endpoint generico documents/getPDFLink
$pdfLink = Moloni::invoices()->getPdfLink($companyId, $invoiceId);
// Disponivel nas guias de transporte/remessa
Moloni::waybills()->setTransportCode($companyId, $waybillId, $code);
Moloni::deliveryNotes()->setTransportCode($companyId, $documentId, $code);
Todos os tipos de documentos
Cada tipo de documento segue a mesma interface. Substitua invoices() pelo metodo correspondente:
Moloni::invoices() // Faturas
Moloni::invoiceReceipts() // Fatura-Recibos
Moloni::simplifiedInvoices() // Faturas Simplificadas
Moloni::receipts() // Recibos
Moloni::creditNotes() // Notas de Credito
Moloni::debitNotes() // Notas de Debito
Moloni::estimates() // Orcamentos
Moloni::purchaseOrders() // Ordens de Compra
Moloni::paymentReturns() // Devolucoes de Pagamento (v2.1+)
Moloni::customerReturnNotes() // Devolucoes de Cliente (v2.1+)
Moloni::internalDocuments() // Documentos Internos (v2.1+)
Moloni::supplierInvoices() // Faturas de Fornecedor
Moloni::supplierSimplifiedInvoices() // Faturas Simplificadas Fornecedor
Moloni::supplierPurchaseOrders() // Ordens de Compra a Fornecedor
Moloni::supplierCreditNotes() // Notas de Credito Fornecedor
Moloni::supplierDebitNotes() // Notas de Debito Fornecedor
Moloni::supplierReturnNotes() // Devolucoes a Fornecedor
Moloni::supplierReceipts() // Recibos de Fornecedor
Moloni::supplierWarrantyRequests() // Pedidos de Garantia
Moloni::deliveryNotes() // Guias de Remessa
Moloni::waybills() // Guias de Transporte
Moloni::billsOfLading() // Cartas de Porte
Moloni::globalGuides() // Guias Globais (v2.1+)
Moloni::ownAssetsMovementGuides() // Movimento de Activos (v2.1+)
Moloni::documents() // Documentos Genericos
Trait HasMoloniDocuments
Associe documentos Moloni a qualquer model Eloquent:
use DigitaldevLx\LaravelMoloni\Concerns\HasMoloniDocuments;
class Order extends Model
{
use HasMoloniDocuments;
}
// Aceder aos documentos
$order->moloniDocuments;
Recursos e Configuracoes
Aceda a todos os recursos auxiliares do Moloni -- impostos, metodos de pagamento, series de documentos, armazens e mais.
$companyId = config('moloni.company_id');
$taxes = Moloni::taxes()->getAll($companyId);
$taxExemptions = Moloni::taxExemptions()->getAll();
$paymentMethods = Moloni::paymentMethods()->getAll($companyId);
$documentSets = Moloni::documentSets()->getAll($companyId);
$warehouses = Moloni::warehouses()->getAll($companyId);
$units = Moloni::measurementUnits()->getAll($companyId);
$maturityDates = Moloni::maturityDates()->getAll($companyId);
$deliveryMethods = Moloni::deliveryMethods()->getAll($companyId);
$bankAccounts = Moloni::bankAccounts()->getAll($companyId);
$caeCodes = Moloni::caeCodes()->getAll($companyId);
$deductions = Moloni::deductions()->getAll($companyId);
$idTemplates = Moloni::identificationTemplates()->getAll($companyId);
$products = Moloni::products()->getAll($companyId);
$categories = Moloni::productCategories()->getAll($companyId);
$stocks = Moloni::productStocks()->getAll($companyId);
$properties = Moloni::productProperties()->getAll($companyId);
$priceClasses = Moloni::priceClasses()->getAll($companyId);
$customers = Moloni::customers()->getAll($companyId);
$customerAltAddresses = Moloni::customerAlternateAddresses()->getAll($companyId);
$suppliers = Moloni::suppliers()->getAll($companyId);
$salesmen = Moloni::salesmen()->getAll($companyId);
$countries = Moloni::countries()->getAll();
$fiscalZones = Moloni::fiscalZones()->getAll($countryId);
$languages = Moloni::languages()->getAll();
$currencies = Moloni::currencies()->getAll();
$currencyExchange = Moloni::currencyExchange()->get($from, $to);
$documentModels = Moloni::documentModels()->getAll();
$multibancoGateways = Moloni::multibancoGateways()->getAll();
Novos recursos em v2.x
A linha v2 completa a cobertura da API Moloni. A v2.0 introduziu 17 novos recursos (Profile, Subscription, Users, CustomerAlternateAddresses, etc.) e a v2.1 acrescentou 12 novos tipos de documentos:
// v2.0 — perfil e conta
$profile = Moloni::myProfile()->get();
$subscription = Moloni::subscription()->get();
$users = Moloni::users()->getAll($companyId);
// v2.0 — auxiliares
$salesmen = Moloni::salesmen()->getAll($companyId);
$vehicles = Moloni::vehicles()->getAll($companyId);
// v2.0 — empresa
$slug = Moloni::companies()->freeSlug($companyId, 'my-slug');
// v2.1 — ciclo de venda alargado
$paymentReturns = Moloni::paymentReturns()->getAll($companyId);
$customerReturns = Moloni::customerReturnNotes()->getAll($companyId);
$internalDocs = Moloni::internalDocuments()->getAll($companyId);
$globalGuides = Moloni::globalGuides()->getAll($companyId);
$ownAssetsGuides = Moloni::ownAssetsMovementGuides()->getAll($companyId);
// v2.1 — ciclo de fornecedor
$supplierCreditNotes = Moloni::supplierCreditNotes()->getAll($companyId);
$supplierDebitNotes = Moloni::supplierDebitNotes()->getAll($companyId);
$supplierReturnNotes = Moloni::supplierReturnNotes()->getAll($companyId);
$supplierReceipts = Moloni::supplierReceipts()->getAll($companyId);
$supplierWarranty = Moloni::supplierWarrantyRequests()->getAll($companyId);
$supplierSimplifiedInv = Moloni::supplierSimplifiedInvoices()->getAll($companyId);
$supplierPurchaseOrders = Moloni::supplierPurchaseOrders()->getAll($companyId);
Data Transfer Objects
O pacote fornece DTOs readonly tipados no namespace DigitaldevLx\LaravelMoloni\DataTransferObjects para garantir type safety:
Eventos
O pacote emite eventos para todas as mutacoes, permitindo reagir a criacoes, atualizacoes e alteracoes de estado de documentos.
| Evento | Trigger |
|---|---|
| DocumentCreated | Documento criado com sucesso |
| DocumentUpdated | Documento actualizado |
| DocumentDeleted | Documento eliminado |
| DocumentCancelled | Documento anulado |
| DocumentClosed | Documento fechado/finalizado |
| CustomerCreated | Novo cliente criado |
| CustomerUpdated | Cliente actualizado |
| CustomerDeleted | Cliente eliminado |
| SupplierCreated | Novo fornecedor criado |
| SupplierUpdated | Fornecedor actualizado |
| SupplierDeleted | Fornecedor eliminado |
| SalesmanCreated | Novo vendedor criado |
| SalesmanUpdated | Vendedor actualizado |
| SalesmanDeleted | Vendedor eliminado |
| ProductCreated | Novo produto criado |
| ProductUpdated | Produto actualizado |
| ProductDeleted | Produto eliminado |
| TokenRefreshed | Token OAuth renovado automaticamente |
Registar listeners
Todos os eventos estao no namespace DigitaldevLx\LaravelMoloni\Events.
Auto-discovery (recomendado): Crie uma classe listener em app/Listeners/ — o Laravel descobre-a automaticamente pelo type-hint do metodo handle():
use DigitaldevLx\LaravelMoloni\Events\DocumentCreated;
class SendInvoiceNotification
{
public function handle(DocumentCreated $event): void
{
// $event->data contem a resposta da API
// $event->documentType contem o tipo de documento (e.g. 'invoices', 'creditNotes')
// — disponivel tambem em DocumentUpdated e DocumentDeleted, permitindo rotear num so listener
}
}
Registo manual: Registe listeners no AppServiceProvider:
use DigitaldevLx\LaravelMoloni\Events\DocumentCreated;
use Illuminate\Support\Facades\Event;
public function boot(): void
{
Event::listen(
DocumentCreated::class,
SendInvoiceNotification::class,
);
}
Closure listeners:
use DigitaldevLx\LaravelMoloni\Events\DocumentCreated;
use Illuminate\Support\Facades\Event;
Event::listen(function (DocumentCreated $event) {
// ...
});
Tratamento de Erros
O pacote fornece excepcoes tipadas para cada cenario de erro, todas a estender MoloniException.
| Excepcao | Cenario |
|---|---|
| AuthenticationException | Erros OAuth2 (credenciais invalidas, token expirado) |
| ValidationException | Falhas de validacao de dados |
| RateLimitException | Rate limit da API atingido (HTTP 429) |
| MoloniException | Todos os outros erros da API |
use DigitaldevLx\LaravelMoloni\Exceptions\MoloniException;
use DigitaldevLx\LaravelMoloni\Exceptions\AuthenticationException;
use DigitaldevLx\LaravelMoloni\Exceptions\ValidationException;
use DigitaldevLx\LaravelMoloni\Exceptions\RateLimitException;
try {
$invoice = Moloni::invoices()->insert($companyId, $data);
} catch (AuthenticationException $e) {
// $e->authError - AuthError enum
// $e->errorDescription - Descricao do erro
Log::error('Moloni auth failed', ['error' => $e->authError]);
} catch (ValidationException $e) {
// $e->errors - Array de erros
// $e->getFieldErrors() - Erros por campo
// $e->hasFieldError('vat') - Verificar campo especifico
return back()->withErrors($e->getFieldErrors());
} catch (RateLimitException $e) {
// Retry apos espera
dispatch(fn () => retry(...))->delay(now()->addMinutes(1));
} catch (MoloniException $e) {
// Erro generico da API
Log::error('Moloni error', ['message' => $e->getMessage()]);
}
Erros de validacao
A ValidationException fornece metodos para inspecionar erros campo a campo:
use DigitaldevLx\LaravelMoloni\Enums\ValidationErrorCode;
try {
$customer = Moloni::customers()->insert($companyId, $data);
} catch (ValidationException $e) {
// Array completo de erros: [['code' => int, 'field' => string, 'description' => string], ...]
$errors = $e->errors;
// Erros organizados por campo
$fieldErrors = $e->getFieldErrors();
// ['vat' => 'NIF portugues invalido', 'email' => 'Endereco de email invalido']
// Verificar se um campo especifico falhou
if ($e->hasFieldError('vat')) {
// Tratar erro de NIF
}
}
Codigos de validacao (enum ValidationErrorCode)
Cada erro vem mapeado para o enum ValidationErrorCode, util para distinguir cenarios programaticamente:
| Codigo | Significado |
|---|---|
| 1 | Campo obrigatorio |
| 2 | Campo numerico invalido |
| 3 | Endereco de email invalido |
| 4 | Valor deve ser unico |
| 5 | Valor invalido |
| 6 | URL invalido |
| 7 | Codigo postal invalido |
| 8 | NIF portugues invalido |
| 9 | Data deve estar no formato AAAA-MM-DD |
| 10 | Associacao de documento invalida |
| 11 | Documento nao pode ser enviado para a AT |
| 12 | Data invalida |
| 13 | Numero de telefone invalido |
| 14 | Artigo tem taxas conflituantes |
| 15 | Artigo tem multiplas entradas de IVA |
| 16 | Identificacao do cliente obrigatoria (Art. 36 CIVA) |
| 17 | Limite de caracteres excedido |
Erros de autenticacao (enum AuthError)
A AuthenticationException expoe $e->authError (enum AuthError) e $e->errorDescription. Valores possiveis:
Boas Praticas
Usar DTOs em vez de arrays
Prefira sempre DTOs tipados a arrays associativos. Os DTOs garantem type safety em compile-time, autocompletar no IDE e validacao implicita da estrutura dos dados.
Tratar todas as excepcoes
A API do Moloni pode retornar erros de autenticacao, validacao ou rate limit. Trate cada tipo de excepcao de forma adequada em vez de capturar apenas MoloniException.
Emitir documentos em background
Use jobs e queues para emitir faturas e outros documentos. Isto evita timeouts em requests HTTP e permite retries automaticos em caso de falha.
// App\Jobs\CreateInvoiceJob.php
class CreateInvoiceJob implements ShouldQueue
{
public function __construct(
private Order $order,
private DocumentDto $document,
) {}
public function handle(): void
{
$companyId = config('moloni.company_id');
Moloni::invoices()->insert($companyId, $this->document);
}
}
// Despachar o job
CreateInvoiceJob::dispatch($order, $documentDto);
Reagir a eventos
Use os eventos do pacote para manter a sua aplicacao sincronizada. Por exemplo, envie um email ao cliente quando uma fatura e criada, ou atualize o stock quando um produto e modificado.
Guardar o company_id na config
Use config('moloni.company_id') em vez de hardcoded IDs. Isto facilita a mudanca entre ambientes e empresas de teste.
Usar HasMoloniDocuments nos models
Adicione a trait HasMoloniDocuments aos models que geram documentos fiscais (encomendas, subscricoes, etc.) para manter uma relacao directa entre os registos da aplicacao e os documentos no Moloni.
Testes
O pacote utiliza Pest PHP v4 (com suporte a Laravel 13), PHPStan para analise estatica e Pint para formatacao de codigo.
# Testes
vendor/bin/pest
# Analise estatica
vendor/bin/phpstan analyse
# Formatacao de codigo
vendor/bin/pint
Mocking em testes de aplicacao
Ao testar a sua aplicacao contra a API Moloni, use Http::fake() para garantir que nenhum pedido real sai da maquina. O host fingido tem de coincidir com MOLONI_BASE_URL:
use Illuminate\Support\Facades\Http;
use DigitaldevLx\LaravelMoloni\Facades\Moloni;
it('creates an invoice for the order', function () {
Http::fake([
'api.moloni.pt/sandbox/grant/*' => Http::response([
'access_token' => 'fake-token',
'refresh_token' => 'fake-refresh',
'expires_in' => 3600,
]),
'api.moloni.pt/sandbox/invoices/insert/*' => Http::response([
'valid' => 1,
'document_id' => 999,
]),
]);
$order = Order::factory()->create();
CreateInvoiceJob::dispatchSync($order);
expect($order->moloniDocuments)->toHaveCount(1);
});
Para um exemplo end-to-end, ver tests/Unit/MoloniClientTest.php no repositorio.
Troubleshooting
Sintomas comuns e como diagnostica-los rapidamente.
| Sintoma | Causa / Solucao |
|---|---|
Repetidos 401 com credenciais correctas |
O token em moloni_tokens foi emitido para outro MOLONI_BASE_URL. Truncar a tabela e re-autenticar. |
AuthenticationException com AuthError::InvalidGrant |
MOLONI_USERNAME / MOLONI_PASSWORD errados. Verificar em moloni.pt/login. |
AuthenticationException com AuthError::InvalidClient |
MOLONI_CLIENT_ID / MOLONI_CLIENT_SECRET errados. Reverificar no Developer Portal. |
RateLimitException |
Moloni devolveu HTTP 429. Aplicar backoff e retry; considerar batching. |
| Chamadas chegam a producao quando se espera sandbox | MOLONI_BASE_URL nao definido ou config em cache. Executar php artisan config:clear. |
Changelog
Versoes e breaking changes
12 novos recursos de documentos & ciclo de fornecedor completo
- Adicionados InternalDocuments, PaymentReturns, OwnAssetsMovementGuides, CustomerReturnNotes, GlobalGuides (lado de venda)
- Adicionado ciclo de fornecedor completo: SupplierSimplifiedInvoices, SupplierPurchaseOrders, SupplierCreditNotes, SupplierDebitNotes, SupplierReturnNotes, SupplierReceipts, SupplierWarrantyRequests
- 10 novos casos de enum
DocumentTypecom labels em portugues - Novo metodo
setTransportCode()nas guias de transporte (codigo AT)
17 novos recursos & cobertura completa de eventos
- 17 novos recursos: MyProfile, Subscription, Users, CustomerAlternateAddresses, Salesmen, Vehicles, Deductions, etc.
- Cobertura completa de eventos para operacoes de mutacao (Created/Updated/Deleted)
Breaking changes
getPdfLink()passa a usar o endpoint genericodocuments/getPDFLinkem vez de caminhos especificos por recurso (assinatura nao muda).DocumentType::PurchaseOrder->valuecorrigido de'purchaseOrder'(singular) para'purchaseOrders'(plural). Dados serializados na base de dados precisam de migracao.
// v1.x: DocumentType::PurchaseOrder->value === 'purchaseOrder'
// v2.0: DocumentType::PurchaseOrder->value === 'purchaseOrders'
// Novo caso adicionado
DocumentType::SupplierInvoice // Faturas de Fornecedor
- Base URL configuravel via
MOLONI_BASE_URL(suporte oficial a sandbox) - Parametros OAuth2 enviados via query string (correccao)
Lancamento inicial. Cobertura completa da API Moloni v1, autenticacao OAuth2 e ~300+ endpoints.
$ composer require digitaldev-lx/laravel-moloni
Pronto para automatizar a faturacao?
Consulte o repositorio no GitHub para a documentacao completa, issues e contribuicoes.