承接 digitaldev-lx/laravel-moloni 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

digitaldev-lx/laravel-moloni

最新稳定版本:2.0.0

Composer 安装命令:

composer require digitaldev-lx/laravel-moloni

包简介

A Laravel package for integrating with the Moloni invoicing API

README 文档

README

Packagist Version Tests PHPStan License

A Laravel package for integrating with the Moloni invoicing API. Provides a clean, fluent interface to manage companies, customers, products, invoices, and all other Moloni resources with full type safety through DTOs and enums.

Official page: digitaldev.pt/packages/laravel-moloni

Requirements

  • PHP 8.4+
  • Laravel 12 or 13

Installation

Install the package via Composer:

composer require digitaldev-lx/laravel-moloni

Publish the configuration file:

php artisan vendor:publish --tag=moloni-config

Run the migrations (required for OAuth token storage):

php artisan migrate

Configuration

Add the following environment variables to your .env file:

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

# Optional. Defaults to production. Override to point at sandbox.
# Production: https://api.moloni.pt/v1/
# Sandbox:    https://api.moloni.pt/sandbox/
MOLONI_BASE_URL=https://api.moloni.pt/v1/

You can obtain your API credentials from the Moloni Developer Portal.

Sandbox

Moloni provides a sandbox environment for testing API integrations without affecting production data. The sandbox is available at https://api.moloni.pt/sandbox/ and exposes the same endpoints as production.

Getting sandbox credentials

  1. You need an active Moloni account. If your login does not yet have access to the demonstration company, request it from apoio@moloni.pt indicating the email associated with your login.
  2. The OAuth2 client_id / client_secret are the same as in production. Only the base_url changes.
  3. Optionally, explore the API interactively at the Moloni API Explorer.

Switching environments

Set MOLONI_BASE_URL in your .env:

# Sandbox
MOLONI_BASE_URL=https://api.moloni.pt/sandbox/

# Production (or omit the variable entirely)
MOLONI_BASE_URL=https://api.moloni.pt/v1/

Important: token persistence between environments

OAuth2 tokens are stored in the moloni_tokens table and are not scoped per environment. When you switch MOLONI_BASE_URL, the existing token is no longer valid for the new host and the package will silently re-authenticate on the next call. To force a clean state, truncate the table:

php artisan tinker --execute="\DigitaldevLx\LaravelMoloni\Models\MoloniToken::truncate();"

A common pattern is to use a separate database for local/testing (already the default with SQLite) so production tokens are never reused against the sandbox host.

Usage

Using the Facade

All API interactions are available through the Moloni facade:

use DigitaldevLx\LaravelMoloni\Facades\Moloni;

Companies

// List all companies
$companies = Moloni::companies()->getAll();

Customers

$companyId = config('moloni.company_id');

// List all customers
$customers = Moloni::customers()->getAll($companyId);

// Find a customer by VAT number
$customer = Moloni::customers()->getByVat($companyId, '123456789');

// Create a customer using a DTO
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);

// Create a customer using an array
$customer = Moloni::customers()->insert($companyId, [
    'vat' => '123456789',
    'number' => 'C001',
    'name' => 'John Doe',
    'email' => 'john@example.com',
]);

Products

// List all products
$products = Moloni::products()->getAll($companyId);

// Find a product by reference
$product = Moloni::products()->getByReference($companyId, 'PROD-001');

// Create a product using a DTO
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);

Invoices

use DigitaldevLx\LaravelMoloni\DataTransferObjects\Document as DocumentDto;
use DigitaldevLx\LaravelMoloni\DataTransferObjects\DocumentProduct;
use DigitaldevLx\LaravelMoloni\DataTransferObjects\Payment;

// Create an invoice
$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);

// Get PDF link for a document
$pdfLink = Moloni::invoices()->getPdfLink($companyId, $invoiceId);

Other Document Types

The package supports all Moloni document types through dedicated resources:

  • Moloni::receipts() - Receipts
  • Moloni::creditNotes() - Credit Notes
  • Moloni::debitNotes() - Debit Notes
  • Moloni::simplifiedInvoices() - Simplified Invoices
  • Moloni::invoiceReceipts() - Invoice Receipts
  • Moloni::deliveryNotes() - Delivery Notes
  • Moloni::billsOfLading() - Bills of Lading
  • Moloni::waybills() - Waybills
  • Moloni::estimates() - Estimates
  • Moloni::documents() - Generic Documents

Settings Resources

Access configuration resources for taxes, payment methods, document sets, and more:

$taxes = Moloni::taxes()->getAll($companyId);
$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);

// Global resources (no company_id needed)
$countries = Moloni::countries()->getAll();
$currencies = Moloni::currencies()->getAll();
$languages = Moloni::languages()->getAll();
$exemptions = Moloni::taxExemptions()->getAll();
$fiscalZones = Moloni::fiscalZones()->getAll($countryId);

Using DTOs

The package provides readonly Data Transfer Objects for type-safe data handling:

  • Customer - Customer data
  • Supplier - Supplier data
  • Salesman - Salesman data
  • Product - Product data
  • ProductStock - Stock movement data
  • Document - Document header data
  • DocumentProduct - Document line item data
  • Payment - Payment data
  • Address - Address data
  • Tax - Tax data
  • Vehicle - Vehicle data
  • Deduction - Withholding tax data
  • PriceClass - Price class data

All DTOs are located in the DigitaldevLx\LaravelMoloni\DataTransferObjects namespace.

Using Events

The package dispatches events for all mutation operations:

Event Dispatched When
DocumentCreated A document is successfully created
DocumentUpdated A document is updated
DocumentDeleted A document is deleted
DocumentCancelled A document is cancelled
DocumentClosed A document is closed/finalized
CustomerCreated A new customer is created
CustomerUpdated A customer is updated
CustomerDeleted A customer is deleted
SupplierCreated A new supplier is created
SupplierUpdated A supplier is updated
SupplierDeleted A supplier is deleted
SalesmanCreated A new salesman is created
SalesmanUpdated A salesman is updated
SalesmanDeleted A salesman is deleted
ProductCreated A new product is created
ProductUpdated A product is updated
ProductDeleted A product is deleted
TokenRefreshed The OAuth token is refreshed

DocumentCreated, DocumentUpdated and DocumentDeleted carry both the API response and a documentType string (e.g. 'invoices', 'creditNotes') so a single listener can route by document type.

All events are located in the DigitaldevLx\LaravelMoloni\Events namespace.

Auto-discovery (recommended)

Create a listener class in your app/Listeners directory. Laravel automatically discovers and registers listeners based on the type-hint in the handle method:

use DigitaldevLx\LaravelMoloni\Events\DocumentCreated;

class SendInvoiceNotification
{
    public function handle(DocumentCreated $event): void
    {
        // $event->data contains the API response
        // $event->documentType contains the document type
    }
}

Manual registration

Register listeners manually in your 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) {
    // ...
});

Error Handling

The package provides granular exception handling that maps directly to Moloni's error system.

Exception Types

Exception When
AuthenticationException OAuth2 errors (invalid credentials, expired tokens, invalid client)
ValidationException Data validation errors (required fields, invalid NIF, invalid email, etc.)
RateLimitException API rate limit exceeded (HTTP 429)
MoloniException All other API errors

All exceptions extend MoloniException, so you can catch them all with a single catch block or handle each type individually.

Authentication Errors

use DigitaldevLx\LaravelMoloni\Exceptions\AuthenticationException;
use DigitaldevLx\LaravelMoloni\Enums\AuthError;

try {
    $customers = Moloni::customers()->getAll($companyId);
} catch (AuthenticationException $e) {
    $e->authError;        // AuthError enum (e.g. AuthError::InvalidGrant)
    $e->errorDescription; // Moloni's error description string
    $e->getMessage();     // Full formatted message
}

Available AuthError enum values: InvalidClient, InvalidUri, RedirectUriMismatch, InvalidRequest, UnsupportedGrantType, UnauthorizedClient, InvalidGrant, InvalidScope.

Validation Errors

use DigitaldevLx\LaravelMoloni\Exceptions\ValidationException;
use DigitaldevLx\LaravelMoloni\Enums\ValidationErrorCode;

try {
    $customer = Moloni::customers()->insert($companyId, $data);
} catch (ValidationException $e) {
    $e->errors;             // Array of ['code' => int, 'field' => string, 'description' => string]
    $e->getFieldErrors();   // ['field_name' => 'description', ...]
    $e->hasFieldError('vat'); // true/false
}

Validation error codes (mapped via ValidationErrorCode enum):

Code Meaning
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

Catching All Errors

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) {
    // Handle auth errors (invalid credentials, expired tokens)
} catch (ValidationException $e) {
    // Handle validation errors (invalid data)
} catch (RateLimitException $e) {
    // Handle rate limiting (retry later)
} catch (MoloniException $e) {
    // Handle all other API errors
}

HasMoloniDocuments Trait

Add the HasMoloniDocuments trait to any Eloquent model to associate it with Moloni documents:

use DigitaldevLx\LaravelMoloni\Concerns\HasMoloniDocuments;

class Order extends Model
{
    use HasMoloniDocuments;
}

// Then use it
$order->moloniDocuments;

Available Resources

Account & Profile

Resource Accessor Method
My Profile Moloni::myProfile()

Company

Resource Accessor Method
Companies Moloni::companies()
Subscription Moloni::subscription()
Users Moloni::users()

Entities

Resource Accessor Method
Customers Moloni::customers()
Customer Alternate Addresses Moloni::customerAlternateAddresses()
Suppliers Moloni::suppliers()
Salesmen Moloni::salesmen()

Products

Resource Accessor Method
Products Moloni::products()
Product Categories Moloni::productCategories()
Product Stocks Moloni::productStocks()
Product Properties Moloni::productProperties()
Price Classes Moloni::priceClasses()

Documents

Resource Accessor Method
Documents Moloni::documents()
Invoices Moloni::invoices()
Receipts Moloni::receipts()
Credit Notes Moloni::creditNotes()
Debit Notes Moloni::debitNotes()
Simplified Invoices Moloni::simplifiedInvoices()
Invoice Receipts Moloni::invoiceReceipts()
Delivery Notes Moloni::deliveryNotes()
Bills of Lading Moloni::billsOfLading()
Waybills Moloni::waybills()
Estimates Moloni::estimates()
Purchase Orders Moloni::purchaseOrders()
Supplier Invoices Moloni::supplierInvoices()

Settings

Resource Accessor Method
Taxes Moloni::taxes()
Tax Exemptions Moloni::taxExemptions()
Payment Methods Moloni::paymentMethods()
Document Sets Moloni::documentSets()
Warehouses Moloni::warehouses()
Measurement Units Moloni::measurementUnits()
Maturity Dates Moloni::maturityDates()
Delivery Methods Moloni::deliveryMethods()
Bank Accounts Moloni::bankAccounts()
CAE Codes Moloni::caeCodes()
Vehicles Moloni::vehicles()
Deductions Moloni::deductions()
Identification Templates Moloni::identificationTemplates()

Global Data

Resource Accessor Method
Countries Moloni::countries()
Fiscal Zones Moloni::fiscalZones()
Languages Moloni::languages()
Currencies Moloni::currencies()
Currency Exchange Moloni::currencyExchange()
Document Models Moloni::documentModels()
Multibanco Gateways Moloni::multibancoGateways()

Testing

Run the package test suite with Pest:

vendor/bin/pest

When testing your own application against the Moloni API, prefer Http::fake() so no real requests leave your machine. The host you fake should match MOLONI_BASE_URL exactly:

use Illuminate\Support\Facades\Http;

Http::fake([
    'api.moloni.pt/sandbox/grant/*' => Http::response([
        'access_token' => 'fake-token',
        'refresh_token' => 'fake-refresh',
        'expires_in' => 3600,
    ]),
    'api.moloni.pt/sandbox/customers/getAll/*' => Http::response([
        ['customer_id' => 1, 'name' => 'Test Customer'],
    ]),
]);

$customers = Moloni::customers()->getAll($companyId);

For an end-to-end example see tests/Unit/MoloniClientTest.php.

Troubleshooting

Symptom Cause / Fix
Repeated 401 after correct credentials The token in moloni_tokens was issued for a different MOLONI_BASE_URL. Truncate the table and re-authenticate.
AuthenticationException with AuthError::InvalidGrant Wrong MOLONI_USERNAME / MOLONI_PASSWORD. Verify against the Moloni client area.
AuthenticationException with AuthError::InvalidClient Wrong MOLONI_CLIENT_ID / MOLONI_CLIENT_SECRET. Re-check at the Developer Portal.
RateLimitException Moloni returned HTTP 429. Back off and retry; consider batching requests.
Calls hit production while you expected sandbox MOLONI_BASE_URL is not set or is cached. Run php artisan config:clear.

Static Analysis

vendor/bin/phpstan analyse

Code Style

vendor/bin/pint

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

License

The MIT License (MIT). Please see License File for more information.

Credits

统计信息

  • 总下载量: 16
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-04-01

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固