tomahock / laravel-moloni
Laravel package for integrating with the Moloni API
Requires
- php: ^8.1
- guzzlehttp/guzzle: ^7.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- mockery/mockery: ^1.5
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- phpunit/phpunit: ^10.0|^11.0
README
Package Laravel para integração completa com a API Moloni.
Índice
- Requisitos
- Instalação
- Configuração
- Autenticação
- Utilização básica
- Entidades
- Produtos
- Documentos
- Configurações da empresa
- Dados globais
- Empresas
- Tratamento de erros
- Testes
- Licença
Requisitos
| Requisito | Versão mínima |
|---|---|
| PHP | 8.1 |
| Laravel | 10, 11 ou 12 |
| Conta Moloni com acesso de developer | — |
Instalação
composer require tomahock/laravel-moloni
O package regista-se automaticamente no Laravel via Package Auto-Discovery. Não é necessário adicionar o provider manualmente.
Configuração
Variáveis de ambiente
Adicione ao seu ficheiro .env as credenciais obtidas na área de developer Moloni:
# Credenciais OAuth (obrigatório) MOLONI_CLIENT_ID=xxxxxxxxxxxxxxxx MOLONI_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx # Tipo de autenticação: 'password' (padrão) ou 'authorization_code' MOLONI_GRANT_TYPE=password # Necessário apenas para o grant type 'password' MOLONI_USERNAME=seuemail@empresa.pt MOLONI_PASSWORD=a_sua_password # Necessário apenas para o grant type 'authorization_code' # MOLONI_REDIRECT_URI=https://a-sua-app.pt/moloni/callback
Publicar o ficheiro de configuração
Para personalizar as opções avançadas, publique o ficheiro de configuração:
php artisan vendor:publish --tag=moloni-config
Isto cria o ficheiro config/moloni.php na sua aplicação.
Referência de todas as opções
// config/moloni.php return [ // Credenciais OAuth obtidas em https://www.moloni.pt/dev/ 'client_id' => env('MOLONI_CLIENT_ID'), 'client_secret' => env('MOLONI_CLIENT_SECRET'), 'redirect_uri' => env('MOLONI_REDIRECT_URI'), // apenas para authorization_code 'username' => env('MOLONI_USERNAME'), // apenas para password grant 'password' => env('MOLONI_PASSWORD'), // apenas para password grant // 'password' ou 'authorization_code' 'grant_type' => env('MOLONI_GRANT_TYPE', 'password'), // URL base da API (não alterar em produção) 'base_url' => env('MOLONI_BASE_URL', 'https://api.moloni.pt/v1'), // Driver de cache para armazenar os tokens OAuth // null = usa o driver padrão da aplicação (config/cache.php) // Exemplos: 'redis', 'memcached', 'file', 'database' 'token_cache_driver' => env('MOLONI_CACHE_DRIVER', null), // Prefixo das chaves de cache dos tokens 'token_cache_prefix' => env('MOLONI_CACHE_PREFIX', 'moloni_token'), // Tempo limite em segundos para cada pedido HTTP 'timeout' => env('MOLONI_TIMEOUT', 30), // Configuração de retry automático em caso de falha de rede 'retry' => [ 'times' => env('MOLONI_RETRY_TIMES', 1), // número de tentativas extra 'sleep' => env('MOLONI_RETRY_SLEEP', 500), // milissegundos entre tentativas ], ];
Autenticação
A API Moloni utiliza OAuth 2.0. O package gere os tokens automaticamente — não precisa de tratar do processo manualmente em cada pedido.
Password Grant — aplicações nativas
O método mais simples. As credenciais (email e password) são trocadas diretamente por um token de acesso. Ideal para integrações internas, scripts de sincronização e aplicações server-to-server onde não há um utilizador humano a fazer login.
Configuração .env:
MOLONI_GRANT_TYPE=password MOLONI_CLIENT_ID=xxxx MOLONI_CLIENT_SECRET=xxxx MOLONI_USERNAME=seuemail@empresa.pt MOLONI_PASSWORD=a_sua_password
Utilização: Após configurar o .env, o package autentica-se automaticamente na primeira chamada à API. Não precisa de fazer mais nada.
use Tomahock\Moloni\Facades\Moloni; // O token é obtido e cacheado automaticamente $clientes = Moloni::company(1)->customers()->getAll();
Authorization Code Grant — aplicações web
Para aplicações onde um utilizador Moloni autoriza a integração através do browser. O fluxo tem dois passos.
Configuração .env:
MOLONI_GRANT_TYPE=authorization_code MOLONI_CLIENT_ID=xxxx MOLONI_CLIENT_SECRET=xxxx MOLONI_REDIRECT_URI=https://a-sua-app.pt/moloni/callback
Passo 1 — Redirecionar o utilizador para o Moloni:
// routes/web.php Route::get('/moloni/auth', function () { return redirect(Moloni::getAuthorizationUrl()); });
O utilizador será enviado para https://www.moloni.pt/ac/root/oauth/ onde irá autenticar-se com a sua conta Moloni e autorizar a aplicação.
Passo 2 — Tratar o callback:
O Moloni redireciona para o MOLONI_REDIRECT_URI com um parâmetro ?code=xxxx na URL.
// routes/web.php Route::get('/moloni/callback', function (Request $request) { $tokens = Moloni::handleAuthorizationCallback($request->code); // Os tokens ficam cacheados automaticamente. // A partir deste momento todas as chamadas à API funcionam. return redirect('/dashboard')->with('success', 'Moloni ligado com sucesso!'); });
O método handleAuthorizationCallback devolve o array de tokens recebido do Moloni:
[
'access_token' => 'xxxxxxxxxxxxxxxx',
'expires_in' => 3600,
'token_type' => 'bearer',
'refresh_token' => 'xxxxxxxxxxxxxxxx',
]
Como os tokens são geridos
O package trata automaticamente de todo o ciclo de vida dos tokens:
| Token | Validade real | Cached durante |
|---|---|---|
| Access token | 1 hora | 55 minutos (margem de segurança) |
| Refresh token | 14 dias | 13 dias |
Fluxo automático:
- Na primeira chamada à API, o token é obtido e guardado em cache.
- Nas chamadas seguintes, o token é lido diretamente da cache — sem pedidos HTTP extra.
- Quando o access token expira, o package usa o refresh token para obter um novo automaticamente.
- Quando o refresh token também expira (ou não existe), o package volta a autenticar com as credenciais configuradas.
- Se um pedido falhar com erro de autenticação, o package limpa a cache e retenta uma vez com um token novo.
Forçar a limpeza dos tokens (útil em testes ou quando as credenciais mudam):
// Via facade app(\Tomahock\Moloni\Http\MoloniAuthenticator::class)->forgetTokens();
Utilização básica
Facade vs injeção de dependência
Pode usar o package de duas formas equivalentes:
Facade (recomendado para a maioria dos casos):
use Tomahock\Moloni\Facades\Moloni; $clientes = Moloni::company(1)->customers()->getAll();
Injeção de dependência:
use Tomahock\Moloni\Moloni; class ClienteController extends Controller { public function __construct(private Moloni $moloni) {} public function index() { return $this->moloni->company(1)->customers()->getAll(); } }
Ambas as abordagens usam a mesma instância singleton registada no container.
Definir a empresa
Obrigatório antes de qualquer chamada a recursos da empresa. O ID da empresa pode ser encontrado na área de configurações do Moloni, ou listando as empresas acessíveis:
// Listar as empresas disponíveis para a conta autenticada $empresas = Moloni::companies()->getAll(); // Resultado: [['company_id' => 123456, 'name' => 'Empresa Lda', ...], ...] // A partir daí, usar o company_id em todas as chamadas Moloni::company(123456)->customers()->getAll();
O método company() devolve a própria instância ($this), pelo que é possível encadear:
$moloni = Moloni::company(123456); $clientes = $moloni->customers()->getAll(); $produtos = $moloni->products()->getAll(); $impostos = $moloni->taxes()->getAll();
Entidades
Clientes
$clientes = Moloni::company(1)->customers();
Listar todos os clientes
$todos = $clientes->getAll(); // Com filtros opcionais $ativos = $clientes->getAll(['active' => 1]);
Contar clientes
$total = $clientes->countAll(); // Resultado: ['count' => 42]
Obter um cliente pelo ID
$cliente = $clientes->getOne(['customer_id' => 5]);
Pesquisar por nome, NIF ou e-mail
// Pesquisa geral (nome, referência interna, etc.) $resultado = $clientes->search('Empresa Lda'); // Por NIF $resultado = $clientes->getByVat('508025338'); // Por e-mail $resultado = $clientes->getByEmail('geral@empresa.pt');
Obter o próximo número de cliente disponível
$numero = $clientes->getNextNumber(); // Resultado: ['next_number' => '00043']
Criar um cliente
$novo = $clientes->insert([ // Campos obrigatórios 'vat' => '999999990', // NIF (use '999999990' para consumidor final) 'name' => 'Empresa Exemplo Lda', 'language_id' => 1, // 1 = Português 'country_id' => 1, // 1 = Portugal 'maturity_date_id' => 1, // ID do prazo de pagamento 'payment_method_id' => 1, // ID do método de pagamento 'salesman_id' => 0, // 0 = sem vendedor 'payment_day' => 0, 'discount' => 0, 'credit_limit' => 0, 'send_options' => 3, // 1=correio, 2=email, 3=ambos // Campos opcionais mas recomendados 'email' => 'geral@empresa.pt', 'address' => 'Rua Exemplo, nº 1', 'zip_code' => '1000-001', 'city' => 'Lisboa', 'phone' => '210000000', 'fax' => '', 'website' => 'https://empresa.pt', 'notes' => 'Cliente VIP', 'number' => '', // deixar vazio para numeração automática ]); // Resultado: ['customer_id' => 99, 'valid' => 1]
Atualizar um cliente
$resultado = $clientes->update([ 'customer_id' => 99, 'name' => 'Empresa Exemplo S.A.', 'email' => 'novo@empresa.pt', // Incluir apenas os campos a alterar, // mas sempre com o customer_id ]);
Eliminar um cliente
$resultado = $clientes->delete(['customer_id' => 99]); // Resultado: ['valid' => 1]
Fornecedores
A interface é idêntica à de clientes.
$fornecedores = Moloni::company(1)->suppliers(); $todos = $fornecedores->getAll(); $umFornecedor = $fornecedores->getOne(['supplier_id' => 3]); $porNif = $fornecedores->getByVat('508025338'); $pesquisa = $fornecedores->search('Distribuidora'); $novo = $fornecedores->insert([ 'vat' => '508025338', 'name' => 'Distribuidora XYZ Lda', 'language_id' => 1, 'country_id' => 1, 'maturity_date_id' => 1, 'payment_method_id' => 1, 'salesman_id' => 0, 'payment_day' => 0, 'discount' => 0, 'credit_limit' => 0, 'send_options' => 2, 'email' => 'compras@distribuidora.pt', 'address' => 'Zona Industrial, Lote 5', 'zip_code' => '2000-001', 'city' => 'Santarém', ]); $fornecedores->update(['supplier_id' => 3, 'phone' => '243000000']); $fornecedores->delete(['supplier_id' => 3]);
Produtos
Produtos
$produtos = Moloni::company(1)->products();
Listar e pesquisar
$todos = $produtos->getAll(); $umProduto = $produtos->getOne(['product_id' => 10]); $porReferencia = $produtos->getByReference('REF-001'); $porCategoria = $produtos->getByCategory(5); $pesquisa = $produtos->search('camisola'); $total = $produtos->countAll();
Criar um produto
$novo = $produtos->insert([ // Obrigatórios 'category_id' => 1, // ID da categoria 'type' => 1, // 1=produto, 2=serviço 'name' => 'Camisola Azul M', 'reference' => 'CAM-AZ-M', 'price' => 29.99, 'unit_id' => 1, // ID da unidade de medida 'has_stock' => 1, // 0=sem gestão de stock, 1=com gestão 'stock' => 50, 'at_product_category' => 'M', // Categoria AT: M=mercadoria, P=produto, S=serviço // Impostos (array de impostos aplicados) 'taxes' => [ [ 'tax_id' => 1, // ID do imposto (IVA 23%) 'value' => 23, // percentagem 'order' => 0, 'cumulative' => 0, ], ], // Campos opcionais 'summary' => 'Camisola de algodão azul, tamanho M', 'notes' => '', 'barcode' => '5601234567890', 'price2' => 0, 'price2_date' => '', 'price3' => 0, 'price3_date' => '', 'price4' => 0, 'price4_date' => '', 'price5' => 0, 'price5_date' => '', 'price6' => 0, 'price6_date' => '', 'exemption_reason' => '', // código de isenção, se IVA = 0% 'warehouse_id' => 1, // armazém principal ]);
Atualizar e eliminar
$produtos->update(['product_id' => 10, 'price' => 24.99, 'stock' => 35]); $produtos->delete(['product_id' => 10]);
Movimentar stock
// Entrada de stock (adicionar) $produtos->updateStock( productId: 10, qty: 20.0, movement: 'add', // 'add' = entrada warehouseId: 1 // opcional; omitir para usar o armazém padrão ); // Saída de stock (subtrair) $produtos->updateStock( productId: 10, qty: 5.0, movement: 'sub' // 'sub' = saída );
Categorias de produtos
$categorias = Moloni::company(1)->productCategories(); $todas = $categorias->getAll(); $umaCategoria = $categorias->getOne(['category_id' => 2]); $nova = $categorias->insert([ 'name' => 'Vestuário', 'parent_id' => 0, // 0 = categoria raiz ]); $categorias->update(['category_id' => 2, 'name' => 'Vestuário e Calçado']); $categorias->delete(['category_id' => 2]);
Documentos
Métodos comuns a todos os documentos
Todos os tipos de documento partilham os seguintes métodos:
| Método | Descrição |
|---|---|
getAll(array $params = []) |
Lista todos os documentos (com filtros opcionais) |
getOne(array $params) |
Obtém um documento pelo document_id |
insert(array $data) |
Cria um novo documento |
update(array $data) |
Atualiza um documento em rascunho |
delete(array $params) |
Elimina um documento em rascunho |
countAll(array $params = []) |
Conta documentos |
getNextNumber(array $params = []) |
Devolve o próximo número disponível para a série |
getByDate(string $inicio, string $fim, array $params = []) |
Filtra por intervalo de datas |
getByCustomer(int $customerId, array $params = []) |
Filtra por cliente |
getPdfLink(int $documentId) |
Obtém o link para download do PDF |
sendEmail(int $documentId, array $emailData) |
Envia o documento por e-mail |
Filtros disponíveis no getAll:
$faturas->getAll([ 'status' => 1, // 0=rascunho, 1=fechado 'customer_id' => 5, 'date' => '2024-01-01', // data de emissão 'number' => 'FT 2024/1', ]);
Filtrar por intervalo de datas:
$faturas = Moloni::company(1)->invoices(); // Documentos de janeiro de 2024 $resultado = $faturas->getByDate('2024-01-01', '2024-01-31'); // Com filtros adicionais $resultado = $faturas->getByDate('2024-01-01', '2024-01-31', ['customer_id' => 5]);
Obter o link do PDF:
$resultado = $faturas->getPdfLink(123); // Resultado: ['url' => 'https://...', 'valid' => 1] $pdfUrl = $resultado['url'];
Enviar por e-mail:
$resultado = $faturas->sendEmail(123, [ 'email' => 'cliente@empresa.pt', 'subject' => 'A sua fatura FT 2024/1', 'message' => 'Segue em anexo a sua fatura. Obrigado pela preferência.', ]);
Faturas
$faturas = Moloni::company(1)->invoices();
Criar uma fatura
$nova = $faturas->insert([ // Cabeçalho — obrigatórios 'date' => '2024-01-15', // data de emissão (YYYY-MM-DD) 'expiration_date' => '2024-02-14', // data de vencimento 'document_set_id' => 1, // ID da série de documentos 'customer_id' => 5, // ID do cliente 'status' => 1, // 0=rascunho, 1=fechado/enviado para AT // Cabeçalho — opcionais 'financial_discount' => 0, // desconto financeiro (%) 'special_discount' => 0, // desconto especial (%) 'salesman_id' => 0, 'salesman_commission' => 0, 'notes' => '', 'our_reference' => '', 'your_reference' => '', // Linhas de produto — obrigatório pelo menos uma 'products' => [ [ 'product_id' => 10, // ID do produto (omitir para linha manual) 'name' => 'Camisola Azul M', 'summary' => '', 'qty' => 2, 'price' => 29.99, 'discount' => 0, // desconto por linha (%) 'order' => 0, // posição na lista (0-based) 'unit_id' => 1, 'exemption_reason' => '', // obrigatório se IVA = 0% 'taxes' => [ [ 'tax_id' => 1, 'value' => 23, 'order' => 0, 'cumulative' => 0, ], ], ], ], // Pagamentos — pode ser vazio em rascunhos 'payments' => [ [ 'payment_method_id' => 1, 'date' => '2024-01-15', 'value' => 73.58, // valor com IVA 'notes' => '', ], ], ]); // Resultado: ['document_id' => 456, 'valid' => 1, 'number' => 'FT 2024/5'] $documentId = $nova['document_id'];
Fluxo completo: criar, obter PDF e enviar
$faturas = Moloni::company(1)->invoices(); // 1. Criar $nova = $faturas->insert([...]); $id = $nova['document_id']; // 2. Obter link do PDF $pdf = $faturas->getPdfLink($id); $url = $pdf['url']; // 3. Enviar ao cliente $faturas->sendEmail($id, [ 'email' => 'cliente@empresa.pt', 'subject' => 'Fatura ' . $nova['number'], 'message' => 'Segue em anexo a sua fatura.', ]);
Faturas simplificadas
Comportamento idêntico às faturas. Indicadas para consumidores finais (NIF 999999990).
$fs = Moloni::company(1)->simplifiedInvoices(); $fs->getAll(); $fs->insert([/* mesmos campos das faturas */]); $fs->getPdfLink($documentId); $fs->sendEmail($documentId, [...]);
Faturas-recibo
Documento que combina fatura e recibo numa só operação.
$fr = Moloni::company(1)->invoiceReceipts(); $fr->getAll(); $fr->insert([ 'date' => '2024-01-15', 'document_set_id' => 1, 'customer_id' => 5, 'status' => 1, 'products' => [/* ... */], 'payments' => [/* ... */], // obrigatório nos faturas-recibo ]);
Recibos
$recibos = Moloni::company(1)->receipts(); $recibos->getAll(); $recibos->getOne(['receipt_id' => 10]); $recibos->insert([ 'date' => '2024-01-15', 'document_set_id' => 1, 'customer_id' => 5, 'status' => 1, 'payments' => [ [ 'payment_method_id' => 1, 'date' => '2024-01-15', 'value' => 100.00, 'notes' => 'Referência: FT 2024/3', ], ], ]);
Notas de crédito
Utilizadas para devoluções ou correções a faturas já emitidas.
$nc = Moloni::company(1)->creditNotes(); $nc->getAll(); $nc->insert([ 'date' => '2024-01-20', 'document_set_id' => 1, 'customer_id' => 5, 'status' => 1, 'products' => [ [ 'product_id' => 10, 'name' => 'Camisola Azul M', 'qty' => 1, 'price' => 29.99, 'discount' => 0, 'order' => 0, 'taxes' => [['tax_id' => 1, 'value' => 23, 'order' => 0, 'cumulative' => 0]], 'exemption_reason' => '', ], ], ]); $nc->getPdfLink($documentId);
Notas de débito
$nd = Moloni::company(1)->debitNotes(); $nd->getAll(); $nd->insert([/* estrutura idêntica às notas de crédito */]);
Orçamentos
$orcamentos = Moloni::company(1)->estimates(); $orcamentos->getAll(); $orcamentos->insert([ 'date' => '2024-01-15', 'expiration_date' => '2024-02-15', // validade do orçamento 'document_set_id' => 1, 'customer_id' => 5, 'status' => 1, 'products' => [/* ... */], ]); $orcamentos->getNextNumber(['document_set_id' => 1]);
Encomendas a clientes
$encomendas = Moloni::company(1)->purchaseOrders(); $encomendas->getAll(); $encomendas->insert([ 'date' => '2024-01-15', 'document_set_id' => 1, 'customer_id' => 5, 'status' => 1, 'products' => [/* ... */], ]);
Guias de remessa
$guias = Moloni::company(1)->deliveryNotes(); $guias->getAll(); $guias->insert([ 'date' => '2024-01-15', 'document_set_id' => 1, 'customer_id' => 5, 'status' => 1, // Dados de transporte (obrigatórios nas guias) 'vehicle_name' => 'XX-00-XX', 'delivery_datetime' => '2024-01-15 09:00', 'delivery_method_id' => 1, // Morada de carga 'ship_from_address' => 'Rua da Empresa, 1', 'ship_from_city' => 'Lisboa', 'ship_from_zip_code' => '1000-001', 'ship_from_country' => 'Portugal', 'ship_from_date' => '2024-01-15', 'ship_from_time' => '09:00', // Morada de descarga 'ship_to_address' => 'Rua do Cliente, 5', 'ship_to_city' => 'Porto', 'ship_to_zip_code' => '4000-001', 'ship_to_country' => 'Portugal', 'products' => [/* ... */], ]);
Guias de transporte (Waybills)
$waybills = Moloni::company(1)->waybills(); $waybills->getAll(); $waybills->insert([/* estrutura idêntica às guias de remessa */]); $waybills->getPdfLink($documentId);
Faturas de fornecedor
$faturasFornecedor = Moloni::company(1)->supplierInvoices(); $faturasFornecedor->getAll(); $faturasFornecedor->getOne(['document_id' => 20]); $nova = $faturasFornecedor->insert([ 'date' => '2024-01-10', 'document_set_id' => 1, 'supplier_id' => 3, // ID do fornecedor (não customer_id) 'status' => 1, 'our_reference' => 'FT/2024/001', 'products' => [/* ... */], 'payments' => [/* ... */], ]);
Configurações da empresa
Impostos
$impostos = Moloni::company(1)->taxes(); $todos = $impostos->getAll(); // Resultado típico: // [ // ['tax_id' => 1, 'name' => 'IVA 23%', 'value' => 23, 'type' => 1, ...], // ['tax_id' => 2, 'name' => 'IVA 13%', 'value' => 13, 'type' => 1, ...], // ['tax_id' => 3, 'name' => 'IVA 6%', 'value' => 6, 'type' => 1, ...], // ] $umImposto = $impostos->getOne(['tax_id' => 1]); $novo = $impostos->insert([ 'name' => 'IVA 23%', 'value' => 23, 'type' => 1, // 1=percentagem 'fiscal_zone' => 'PT', 'active_by_default' => 1, ]); $impostos->update(['tax_id' => 1, 'active_by_default' => 0]); $impostos->delete(['tax_id' => 5]);
Métodos de pagamento
$metodos = Moloni::company(1)->paymentMethods(); $todos = $metodos->getAll(); // [ // ['payment_method_id' => 1, 'name' => 'Numerário'], // ['payment_method_id' => 2, 'name' => 'Transferência Bancária'], // ['payment_method_id' => 3, 'name' => 'Multibanco'], // ] $novo = $metodos->insert(['name' => 'PayPal']); $metodos->update(['payment_method_id' => 4, 'name' => 'PayPal / Stripe']); $metodos->delete(['payment_method_id' => 4]);
Armazéns
$armazens = Moloni::company(1)->warehouses(); $todos = $armazens->getAll(); // [['warehouse_id' => 1, 'name' => 'Armazém Principal', 'is_default' => 1, ...]] $novo = $armazens->insert([ 'name' => 'Armazém Secundário', 'address' => 'Zona Industrial, Lote 3', 'zip_code' => '2000-001', 'city' => 'Santarém', 'country_id' => 1, ]); $armazens->update(['warehouse_id' => 2, 'name' => 'Armazém Norte']); $armazens->delete(['warehouse_id' => 2]);
Unidades de medida
$unidades = Moloni::company(1)->measurementUnits(); $todas = $unidades->getAll(); // [ // ['unit_id' => 1, 'name' => 'Unidade (un)'], // ['unit_id' => 2, 'name' => 'Quilograma (kg)'], // ['unit_id' => 3, 'name' => 'Metro (m)'], // ] $nova = $unidades->insert(['name' => 'Litro (L)', 'short_name' => 'L']); $unidades->update(['unit_id' => 5, 'name' => 'Litro']); $unidades->delete(['unit_id' => 5]);
Séries de documentos
As séries determinam a numeração dos documentos (ex: "FT 2024/...").
$series = Moloni::company(1)->documentSets(); $todas = $series->getAll(); // [['document_set_id' => 1, 'name' => 'Série 2024', 'active' => 1, ...]] $nova = $series->insert([ 'name' => 'Série 2025', 'template_id' => 1, // ID do template de impressão ]); $series->update(['document_set_id' => 2, 'name' => 'Série Exportação 2025']);
Contas bancárias
$contas = Moloni::company(1)->bankAccounts(); $todas = $contas->getAll(); $nova = $contas->insert([ 'name' => 'Conta CGD', 'iban' => 'PT50003506510007341000358', 'swift' => 'CGDIPTPL', 'initial_balance' => 0, ]); $contas->update(['bank_account_id' => 1, 'name' => 'CGD — Conta Principal']); $contas->delete(['bank_account_id' => 3]);
Dados globais
Estes endpoints não requerem company_id — podem ser chamados diretamente sem ::company().
Países
$paises = Moloni::countries()->getAll(); // [ // ['country_id' => 1, 'name' => 'Portugal', 'iso' => 'PT'], // ['country_id' => 2, 'name' => 'Espanha', 'iso' => 'ES'], // ... // ] // Com filtro $portugal = Moloni::countries()->getAll(['search' => 'Portugal']);
Moedas
$moedas = Moloni::currencies()->getAll(); // [ // ['currency_id' => 1, 'name' => 'Euro', 'iso' => 'EUR', 'symbol' => '€'], // ['currency_id' => 2, 'name' => 'Dólar', 'iso' => 'USD', 'symbol' => '$'], // ... // ]
Empresas
// Listar todas as empresas às quais a conta autenticada tem acesso // (útil para descobrir o company_id correto) $empresas = Moloni::companies()->getAll(); // [ // ['company_id' => 123456, 'name' => 'Empresa A Lda', 'vat' => '...'], // ['company_id' => 789012, 'name' => 'Empresa B Unip.', 'vat' => '...'], // ] // Obter os detalhes de uma empresa específica $empresa = Moloni::company(123456)->companies()->getOne(); // Ou especificando o ID explicitamente: $empresa = Moloni::companies()->getOne(['company_id' => 123456]); // Atualizar dados da empresa Moloni::company(123456)->companies()->update([ 'email' => 'geral@empresa.pt', 'phone' => '210000000', 'website' => 'https://empresa.pt', ]);
Tratamento de erros
O package lança duas exceções específicas, ambas em Tomahock\Moloni\Exceptions:
| Exceção | Quando é lançada |
|---|---|
MoloniAuthException |
Falha de autenticação OAuth (credenciais inválidas, token expirado irrecuperável) |
MoloniException |
A API devolveu um erro lógico (valid = 0) ou a resposta não é JSON válido |
MoloniAuthException estende MoloniException, pelo que pode capturar ambas com um único catch (MoloniException $e).
Estrutura básica
use Tomahock\Moloni\Exceptions\MoloniAuthException; use Tomahock\Moloni\Exceptions\MoloniException; use Tomahock\Moloni\Facades\Moloni; try { $resultado = Moloni::company(1)->customers()->insert([...]); } catch (MoloniAuthException $e) { // Problema de autenticação — verificar credenciais ou tokens Log::error('Moloni auth error: ' . $e->getMessage()); } catch (MoloniException $e) { // Erro lógico devolvido pela API Log::error('Moloni API error: ' . $e->getMessage()); // Detalhes do erro (array com os erros reportados pelo Moloni) $erros = $e->getErrors(); // Exemplo: [['message' => 'O NIF introduzido já existe'], ...] }
Inspecionar os erros da API
Quando o Moloni devolve valid = 0, a exceção MoloniException contém os detalhes no método getErrors():
try { Moloni::company(1)->customers()->insert(['vat' => '999999990', ...]); } catch (MoloniException $e) { foreach ($e->getErrors() as $erro) { echo $erro['message']; // ex: "O campo email é inválido" } }
Exemplo com validação no controlador
public function store(Request $request) { try { $cliente = Moloni::company(config('services.moloni.company_id')) ->customers() ->insert($request->validated()); return response()->json(['id' => $cliente['customer_id']], 201); } catch (MoloniAuthException $e) { return response()->json(['error' => 'Erro de autenticação Moloni'], 503); } catch (MoloniException $e) { return response()->json([ 'error' => 'Erro ao criar cliente no Moloni', 'detail' => $e->getMessage(), 'errors' => $e->getErrors(), ], 422); } }
Testes
Correr os testes do package
composer test
Testar a integração na sua aplicação
Nos testes da sua aplicação, pode fazer mock do package usando o sistema de mocking do Laravel:
use Tomahock\Moloni\Facades\Moloni; use Tomahock\Moloni\Resources\Customers; // No teste Moloni::shouldReceive('company')->with(1)->andReturnSelf(); Moloni::shouldReceive('customers')->andReturn( tap(Mockery::mock(Customers::class), function ($mock) { $mock->shouldReceive('getAll')->andReturn([ ['customer_id' => 1, 'name' => 'Cliente Teste'], ]); }) );
Ou, alternando para uma implementação fake no AppServiceProvider:
// app/Providers/AppServiceProvider.php public function register(): void { if (app()->environment('testing')) { $this->app->bind(\Tomahock\Moloni\Moloni::class, \App\Fakes\FakeMoloni::class); } }
Licença
MIT