PHPackages                             tomahock/laravel-moloni - 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. [API Development](/categories/api)
4. /
5. tomahock/laravel-moloni

ActiveLibrary[API Development](/categories/api)

tomahock/laravel-moloni
=======================

Laravel package for integrating with the Moloni API

v1.0.3(2mo ago)17MITPHP ^8.1

Since Apr 21Compare

[ Source](https://github.com/tomahock/laravel-moloni)[ Packagist](https://packagist.org/packages/tomahock/laravel-moloni)[ Docs](https://github.com/tomahock/laravel-moloni)[ RSS](/packages/tomahock-laravel-moloni/feed)WikiDiscussions Synced 3w ago

READMEChangelogDependencies (6)Versions (5)Used By (0)

Laravel Moloni
==============

[](#laravel-moloni)

Package Laravel para integração completa com a [API Moloni](https://www.moloni.pt/dev/).

---

Índice
------

[](#índice)

- [Requisitos](#requisitos)
- [Instalação](#instala%C3%A7%C3%A3o)
- [Configuração](#configura%C3%A7%C3%A3o)
    - [Variáveis de ambiente](#vari%C3%A1veis-de-ambiente)
    - [Publicar o ficheiro de configuração](#publicar-o-ficheiro-de-configura%C3%A7%C3%A3o)
    - [Referência de todas as opções](#refer%C3%AAncia-de-todas-as-op%C3%A7%C3%B5es)
- [Autenticação](#autentica%C3%A7%C3%A3o)
    - [Password Grant — aplicações nativas](#password-grant--aplica%C3%A7%C3%B5es-nativas)
    - [Authorization Code Grant — aplicações web](#authorization-code-grant--aplica%C3%A7%C3%B5es-web)
    - [Como os tokens são geridos](#como-os-tokens-s%C3%A3o-geridos)
- [Utilização básica](#utiliza%C3%A7%C3%A3o-b%C3%A1sica)
    - [Facade vs injeção de dependência](#facade-vs-inje%C3%A7%C3%A3o-de-depend%C3%AAncia)
    - [Definir a empresa](#definir-a-empresa)
- [Entidades](#entidades)
    - [Clientes](#clientes)
    - [Fornecedores](#fornecedores)
- [Produtos](#produtos)
    - [Produtos](#produtos-1)
    - [Categorias de produtos](#categorias-de-produtos)
- [Documentos](#documentos)
    - [Métodos comuns a todos os documentos](#m%C3%A9todos-comuns-a-todos-os-documentos)
    - [Faturas](#faturas)
    - [Faturas simplificadas](#faturas-simplificadas)
    - [Faturas-recibo](#faturas-recibo)
    - [Recibos](#recibos)
    - [Notas de crédito](#notas-de-cr%C3%A9dito)
    - [Notas de débito](#notas-de-d%C3%A9bito)
    - [Orçamentos](#or%C3%A7amentos)
    - [Encomendas a clientes](#encomendas-a-clientes)
    - [Guias de remessa](#guias-de-remessa)
    - [Guias de transporte](#guias-de-transporte)
    - [Faturas de fornecedor](#faturas-de-fornecedor)
- [Configurações da empresa](#configura%C3%A7%C3%B5es-da-empresa)
    - [Impostos](#impostos)
    - [Métodos de pagamento](#m%C3%A9todos-de-pagamento)
    - [Armazéns](#armaz%C3%A9ns)
    - [Unidades de medida](#unidades-de-medida)
    - [Séries de documentos](#s%C3%A9ries-de-documentos)
    - [Contas bancárias](#contas-banc%C3%A1rias)
- [Dados globais](#dados-globais)
    - [Países](#pa%C3%ADses)
    - [Moedas](#moedas)
- [Empresas](#empresas)
- [Tratamento de erros](#tratamento-de-erros)
- [Testes](#testes)
- [Licença](#licen%C3%A7a)

---

Requisitos
----------

[](#requisitos)

RequisitoVersão mínimaPHP8.1Laravel10, 11 ou 12Conta Moloni com acesso de developer—---

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

[](#instalação)

```
composer require tomahock/laravel-moloni
```

O package regista-se automaticamente no Laravel via [Package Auto-Discovery](https://laravel.com/docs/packages#package-discovery). Não é necessário adicionar o provider manualmente.

---

Configuração
------------

[](#configuração)

### Variáveis de ambiente

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

Adicione ao seu ficheiro `.env` as credenciais obtidas na [área de developer Moloni](https://www.moloni.pt/dev/):

```
# 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

[](#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

[](#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
------------

[](#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

[](#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

[](#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

[](#como-os-tokens-são-geridos)

O package trata automaticamente de todo o ciclo de vida dos tokens:

TokenValidade realCached duranteAccess token1 hora55 minutos (margem de segurança)Refresh token14 dias13 dias**Fluxo automático:**

1. Na primeira chamada à API, o token é obtido e guardado em cache.
2. Nas chamadas seguintes, o token é lido diretamente da cache — sem pedidos HTTP extra.
3. Quando o access token expira, o package usa o refresh token para obter um novo automaticamente.
4. Quando o refresh token também expira (ou não existe), o package volta a autenticar com as credenciais configuradas.
5. 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
-----------------

[](#utilização-básica)

### Facade vs injeção de dependência

[](#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

[](#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
---------

[](#entidades)

### Clientes

[](#clientes)

```
$clientes = Moloni::company(1)->customers();
```

#### Listar todos os clientes

[](#listar-todos-os-clientes)

```
$todos = $clientes->getAll();

// Com filtros opcionais
$ativos = $clientes->getAll(['active' => 1]);
```

#### Contar clientes

[](#contar-clientes)

```
$total = $clientes->countAll();
// Resultado: ['count' => 42]
```

#### Obter um cliente pelo ID

[](#obter-um-cliente-pelo-id)

```
$cliente = $clientes->getOne(['customer_id' => 5]);
```

#### Pesquisar por nome, NIF ou e-mail

[](#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

[](#obter-o-próximo-número-de-cliente-disponível)

```
$numero = $clientes->getNextNumber();
// Resultado: ['next_number' => '00043']
```

#### Criar um cliente

[](#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

[](#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

[](#eliminar-um-cliente)

```
$resultado = $clientes->delete(['customer_id' => 99]);
// Resultado: ['valid' => 1]
```

---

### Fornecedores

[](#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

[](#produtos-1)

```
$produtos = Moloni::company(1)->products();
```

#### Listar e pesquisar

[](#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

[](#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

[](#atualizar-e-eliminar)

```
$produtos->update(['product_id' => 10, 'price' => 24.99, 'stock' => 35]);
$produtos->delete(['product_id' => 10]);
```

#### Movimentar stock

[](#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-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
----------

[](#documentos)

### Métodos comuns a todos os documentos

[](#métodos-comuns-a-todos-os-documentos)

Todos os tipos de documento partilham os seguintes métodos:

MétodoDescriçã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)

```
$faturas = Moloni::company(1)->invoices();
```

#### Criar uma fatura

[](#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

[](#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

[](#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

[](#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)

```
$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

[](#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

[](#notas-de-débito)

```
$nd = Moloni::company(1)->debitNotes();

$nd->getAll();
$nd->insert([/* estrutura idêntica às notas de crédito */]);
```

---

### Orçamentos

[](#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-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-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)

[](#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

[](#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
------------------------

[](#configurações-da-empresa)

### Impostos

[](#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

[](#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

[](#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-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

[](#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-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
-------------

[](#dados-globais)

Estes endpoints não requerem `company_id` — podem ser chamados diretamente sem `::company()`.

### Países

[](#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)

```
$moedas = Moloni::currencies()->getAll();
// [
//   ['currency_id' => 1, 'name' => 'Euro',   'iso' => 'EUR', 'symbol' => '€'],
//   ['currency_id' => 2, 'name' => 'Dólar',  'iso' => 'USD', 'symbol' => '$'],
//   ...
// ]
```

---

Empresas
--------

[](#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
-------------------

[](#tratamento-de-erros)

O package lança duas exceções específicas, ambas em `Tomahock\Moloni\Exceptions`:

ExceçãoQuando é 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

[](#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

[](#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

[](#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
------

[](#testes)

### Correr os testes do package

[](#correr-os-testes-do-package)

```
composer test
```

### Testar a integração na sua aplicação

[](#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
-------

[](#licença)

MIT

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance84

Actively maintained with recent releases

Popularity6

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity46

Maturing project, gaining track record

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.

###  Release Activity

Cadence

Every ~4 days

Total

4

Last Release

81d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/794581?v=4)[João Pina](/maintainers/tomahock)[@tomahock](https://github.com/tomahock)

---

Tags

apilaravelbillinginvoicingportugalMoloni

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/tomahock-laravel-moloni/health.svg)

```
[![Health](https://phpackages.com/badges/tomahock-laravel-moloni/health.svg)](https://phpackages.com/packages/tomahock-laravel-moloni)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[simplestats-io/laravel-client

Server-side analytics for Laravel that follows the full funnel from visit to registration to payment, attributed to the channel that drove it. Revenue, MRR, churn and ad-spend profit (ROAS/CAC) per channel. GDPR compliant, ad-blocker proof.

5022.6k](/packages/simplestats-io-laravel-client)[defstudio/telegraph

A laravel facade to interact with Telegram Bots

813336.8k3](/packages/defstudio-telegraph)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[eslazarev/wildberries-sdk

Wildberries OpenAPI clients (generated).

293.1k](/packages/eslazarev-wildberries-sdk)

PHPackages © 2026

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