Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Facturax Contracts Laravel Package

carloschininin/facturax-contracts

Versioned DTO contracts for the Facturax API, with an optional Symfony bundle that registers validation and normalization services. Covers invoices, credit notes, daily summaries, voided documents, shipping guides, perceptions, retentions, and reversals, keeping payloads decoupled from Greenter.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is a contract-only library: it gives you versioned DTOs for Facturax payloads plus two services (ContractValidator, ContractNormalizer). It does not sign, authenticate, or call SUNAT/OSE — that stays in Facturax or your transport layer.

Install

composer require carloschininin/facturax-contracts

Before touching code, read in this order:

  1. docs/examples/v1/*.json — the eight canonical payloads (comprobante, nota, resumen, baja, guía, percepción, retención, reversión). These tell you more about the contract than the README.
  2. src/Api/V1/Dto/ — the DTO namespace tree (Comprobante, Nota, Resumen, Baja, Guia, Percepcion, Retencion, Reversion).
  3. CarlosChinininFacturaxContractsBundle — only relevant if you actually run Symfony; in Laravel you won't register it.

Skip the bundle in Laravel. The Symfony bundle registers the two services in a Symfony DI container. Laravel doesn't consume Symfony bundles, so bind them yourself:

// app/Providers/AppServiceProvider.php
public function register(): void
{
    $this->app->singleton(\CarlosChininin\FacturaxContracts\Service\ContractValidator::class);
    $this->app->singleton(\CarlosChininin\FacturaxContracts\Service\ContractNormalizer::class);
}

Your first real use case: validate a factura before it leaves your app. The most common flow is:

use CarlosChininin\FacturaxContracts\Api\V1\Dto\Comprobante\ComprobanteRequestDto;
use CarlosChininin\FacturaxContracts\Service\{ContractNormalizer, ContractValidator};

$dto = ComprobanteRequestDto::fromArray($payload); // hydrate from your request/array
$dto = app(ContractNormalizer::class)->normalize($dto);
app(ContractValidator::class)->validate($dto);     // throws on contract violation

Http::withToken($token)->post(config('services.facturax.url').'/v1/comprobantes', $dto->toArray());

That "hydrate → normalize → validate → POST" chain is the whole package in one line of thinking. Everything below is refinement of it.

Implementation Patterns

1. Wrap the DTO lifecycle in one service, not in controllers.

Create App\Billing\FacturaxPayload (or similar) that exposes a single method per document family:

final class FacturaxPayload
{
    public function __construct(
        private ContractNormalizer $normalizer,
        private ContractValidator $validator,
    ) {}

    public function prepare(ComprobanteRequestDto $dto): array
    {
        return $this->validator
            ->validate($this->normalizer->normalize($dto))
            ->toArray();
    }
}

This keeps the contract layer out of HTTP controllers and out of jobs, and gives you one seam to swap when Api\V2 appears.

2. Hydrate from your own domain, never blindly from $request->all().

DTOs are strict typed objects. If you hand them an array shaped by a Blade form or an external webhook, a missing/null field will surface as a type error deep inside the DTO constructor rather than a clean validation message. Use a dedicated FormRequest first (Laravel-native rules: required|numeric|min:0, date_format:Y-m-d, RUC size:11) and build the DTO from $request->validated(). The ContractValidator is your second gate — it enforces Facturax semantics (totals consistency, retención on facturas, etc.), not Laravel HTTP validation.

3. Normalize before you validate, always.

ContractNormalizer is where string→decimal coercion, date formatting, and omission defaults happen. Validating a raw, un-normalized DTO will produce false negatives on fields the normalizer would have filled or cast. If a validator complaint feels wrong, log the normalized DTO, not the input.

4. Use the shared objects as first-class values.

Receptor/dirección, totales, forma de pago/cuotas, cargos/descuentos, leyendas, anticipos, detracción, percepción, pagos/tipo de cambio, and detalle attributes are all reusable DTOs. Build them once in a helper (e.g., ReceptorDto::fromCustomer($customer)), unit-test them, and reuse across comprobante, nota, and guía payloads. This is where the day-to-day savings actually come from — not the validator.

5. Queue the dispatch, not the build.

Payload construction is cheap and synchronous. What you want async is the POST to Facturax:

final class SendComprobante implements ShouldQueue
{
    public function __construct(public array $payload) {}

    public function handle(FacturaxClient $client): void
    {
        $client->post('/v1/comprobantes', $this->payload);
    }
}

This way a DTO/contract failure happens in the request (fast, user-visible, retryable via form), while network failures get queue retries. Don't blend the two failure modes.

6. Pin to Api\V1 explicitly and treat it as an import boundary.

The DTOs live under Api\V1\Dto\... for a reason. When V2 lands, you should be able to use from Api\V2 in one class per document type and migrate gradually. Never alias V1 classes into your domain model — always map to your own value objects at the seam.

7. Reuse docs/examples/v1/*.json as test fixtures.

Load the example JSON in a Pest/PHPUnit dataset and assert normalize(validate(fromArray(json)))->toArray() returns a structurally identical payload. That gives you a regression suite for free every time you bump the package.

8. Idempotency at the caller, not the contract.

The contract has no notion of an idempotency key or external reference (except what you put in leyendas or detail attributes). If you retry a factura you can double-issue. Add your own uuid to the job and store the Facturax response against it in facturax_submissions so retries are safe.

Gotchas and Tips

Version constraints in the README do not match the changelog. The README says PHP 8.5+ / Symfony 8.x, while release/0.0.2.md advertises PHP 8.3 / Symfony 7.1. Verify with composer why-not carloschininin/facturax-contracts and inspect the real composer.json constraints before assuming a given PHP version works. If you're on PHP 8.3/8.4 and the README is accurate, you can't install it — check --with-all-dependencies and consider a constraint override on a scratch branch first.

The Symfony bundle is a trap in Laravel. CarlosChinininFacturaxContractsBundle requires symfony/http-kernel as a hard dependency at load time. Don't add it to bootstrap/providers.php. Bind ContractValidator and ContractNormalizer manually (see Getting Started). If you see "Class not found: Bundle" during artisan optimize, you've accidentally auto-discovered it via a config/bundles.php-style file — remove it.

The emisor is not in the contract. ComprobanteRequestDto has no emisor block with credentials, cert, or SOL user. Facturax injects those server-side from your authenticated account. Don't try to stuff RUC/cert into receptor or attributes — the validator won't reject it, but Facturax will. Same for endpoints and OSE routing.

Validation likely throws, not returns. ContractValidator is a Symfony-style service; expect it to throw (e.g., ValidationException or a Symfony Validator exception, depending on internals) rather than return a boolean. Wrap it in a Laravel handler that converts to ValidationException::withMessages(...) so your API returns 422 instead of 500. Log the normalized DTO alongside the exception message — the messages reference DTO paths, not array keys.

Normalizer is not a sanitizer. It coerces decimals and dates, but it will not fix a wrong SUNAT catalog code (e.g., 01 factura vs 03 boleta), a bad tipoDocIdentidad, or an invalid ubigeo. Those live in shared objects and must be correct at construction time. Keep your own enum for tipo_comprobante and map in.

Dates and decimals follow SUNAT conventions, not Carbon defaults. Expect Y-m-d for issue date, Y-m-d for summary reference dates, and decimal(10,2)-ish precision. If you're storing totals as floats in your DB, round-and-cast before creating the DTO — the validator checks totals consistency, and float drift will produce "totales no coinciden" errors that are maddening to debug.

A single "comprobante" family is really a family of subtrees. ComprobanteRequestDto is a union of factura/boleta and everything from formaPago → cuotas, detraccion, anticipos, cargos/descuentos, percepcion. A payload that passes for a factura with no detracción may silently accept a boleta with detracción, which SUNAT rejects. Test both shapes — the examples only show factura.

Ecosystem risk is real. 1 star, 0 dependents, last release 2026-07-22 (a future-dated release; treat it as unreleased or pre-release), and a score around 17/100. In practice:

  • Commit composer.lock.
  • Add a composer audit gate in CI.
  • Wrap the package behind your FacturaxPayload service (pattern #1) so you can fork or replace it without touching call sites.
  • Subscribe to releases; watch for the first Api\V2 namespace — that's the signal a breaking contract change is in flight.

Extension points if you need to go beyond the built-ins. ContractValidator and ContractNormalizer are plain classes bound in the container — you can decorate them:

$this->app->extend(ContractValidator::class, fn ($inner) => new class($inner) {
    public function __construct(private ContractValidator $inner) {}
    public function validate($dto) {
        $dto = $this->inner->validate($dto);
        // + your own rules, e.g. cross-check against local customer catalog
        return $dto;
    }
});

Use this for business rules (credit limit, active customer, series ownership). Don't extend ContractNormalizer — it's doing deterministic coercion and you'll shadow bugs in the base package that you can't easily see.

Run vendor/bin/phpunit from the package first, in a scratch clone. The README documents this. If tests fail on your PHP version, you'll know before you've written a single line of integration code — and it costs you 2 minutes instead of a debugging session.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table
siberfx/flysystem-wasabi
hollyit/token-replace
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel