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

Nfse Ssa Laravel Package

potelo/nfse-ssa

A Laravel package for issuing and managing NFS-e (electronic service invoices) through Salvador's municipal web service, wrapping the SOAP API in a fluent, configurable interface for service providers and invoice data.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and publish

composer require potelo/nfse-ssa
php artisan vendor:publish --provider="Potelo\NfseSsa\NfseSsaServiceProvider"

The package is a thin wrapper around Salvador's municipal NFS-e SOAP webservice, so "getting started" is really two separate tasks: making Composer happy (5 minutes) and making the prefeitura happy (a day or two). Budget accordingly.

Where to look first, in this order

  1. config/nfse-ssa.php after publishing — every environment-specific value lives here (endpoint URLs, inscrição municipal, CNPJ do prestador, certificate path/password, RPS series). Read it top to bottom before writing a single line of application code.
  2. src/ — the package is small enough to read entirely in one sitting (~a few classes). Do it. With 25 stars and zero dependents there is no Stack Overflow corpus for this thing; the source is the documentation.
  3. README.md examples — treat as a starting sketch, not a spec. Verify every method signature against src/ before trusting the snippet.
  4. Any tests/ or fixture XML in the repo — these are the most valuable artifact, because they show the exact envelope the municipality's webservice actually accepts.

Prerequisites outside the package

  • An A1 digital certificate (.pfx/.p12) valid for the CNPJ that emits the invoice. A1 only — A3 hardware tokens will not work in a headless PHP process.
  • ext-openssl, ext-soap, ext-dom/ext-libxml enabled. SOAP is non-negotiable here; if your host blocks outbound SOAP, stop now.
  • The inscrição municipal and the código do serviço (the municipal service list code, not the federal CNAE) for each service you bill. These are the two fields most likely to cause a rejected RPS on day one.

First use case to build

Do not start with your real billing flow. Build a one-off Artisan command that emits a single RPS against the homologation / test endpoint, dumps the raw request XML and raw response XML to storage/logs/, and exits. Run it until you get an accepted response. Only then wire it into application code. Getting a green response once tells you the certificate, signature, service code, and endpoint config are all correct — isolating that from your business logic saves hours.

Implementation Patterns

Wrap it, never call it from controllers

The package gives you a client. Your application needs a domain concept: EmitirNfse, CancelarNfse, ConsultarNfse. Put a single class between your controllers/jobs and the package so that when you eventually fork or swap the package (see Gotchas), the blast radius is one file.

final class EmitirNfse
{
    public function __construct(private NfseSsa $nfse) {}

    public function __invoke(NotaFiscal $nota): ResultadoEmissao
    {
        // map domain -> package payload
        // call the client
        // persist raw XML out + raw XML in
        // return a domain result, not a SOAP response
    }
}

Persist the raw XML — both directions

For every request, store xml_envio and xml_retorno as LONGTEXT alongside the invoice row. Not optional. Brazilian municipal systems return terse, sometimes misleading error strings; when a client disputes an invoice six months later, the signed XML is your only defensible evidence. It also makes debugging possible without re-issuing (which you cannot do casually — see below).

Model RPS numbering as a database sequence, not a config value

The RPS number must be unique per series per CNPJ and must be monotonically increasing with no gaps. A config value incremented in PHP will collide under concurrent requests. Use a dedicated table with SELECT ... FOR UPDATE, or a Postgres sequence, and reserve the number inside the same transaction that creates the invoice row.

DB::transaction(function () use ($nota) {
    $numero = DB::table('rps_sequence')->lockForUpdate()->first()->proximo;
    DB::table('rps_sequence')->increment('proximo');
    // persist nota with $numero, then dispatch the emission job
});

If you emit with a gap in the sequence, expect a rejected batch at the worst possible moment.

Emit from a queued job with bounded retries

final class EmitirNfseJob implements ShouldQueue
{
    public int $tries = 3;
    public int $backoff = [60, 300, 900];

    public function handle(EmitirNfse $action): void { /* ... */ }
}

The municipal webservice has real downtime — maintenance windows, timeouts, and the occasional 500 with an HTML body instead of SOAP. Retries are essential. But retries are dangerous: a timeout does not mean the invoice wasn't issued. Always attempt a consult by RPS number before re-emitting. The safe pattern is:

  1. Attempt emission.
  2. On transport-level failure (timeout, 5xx, unparseable response), do not blindly retry — first run a consultation for that RPS.
  3. Only re-emit if the consultation confirms nothing was recorded.

Separate "rejected" from "failed"

Municipality responses fall into distinct buckets, and they need different handling:

  • Accepted → persist the number, the verification code, and the issued XML; notify the customer.
  • Rejected (validation) → business error. Bad service code, missing tomador document, ISS retention mismatch. Do not retry; surface to an operator with the raw message.
  • Transport failure → retry with consultation-first logic above.
  • Signature/certificate error → alert loudly. This is infrastructure, and every subsequent invoice will fail too.

Certificate handling in production

Keep the .pfx and its passphrase out of the repo and out of the config file. Load both from environment/secret manager, and resolve the path at runtime rather than hardcoding. Build a scheduled check that parses the certificate and alerts 30 days before expiry — a silently expired A1 certificate fails every emission with an unhelpful error.

$cert = openssl_x509_parse(file_get_contents(storage_path('cert/empresa.pfx')));
// assert validTo > now + 30 days

Fake it in tests

Never let a test suite touch the real endpoint. Either bind a fake implementation of your EmitirNfse interface in the container, or record real request/response XML pairs from homologation and replay them from fixtures. Because the package is a thin wrapper, mocking your wrapper is cheaper and more stable than mocking the SOAP layer.

Multi-tenant / multi-CNPJ

If you're building a SaaS, the certificate and inscrição municipal are per-tenant, which means the config file must become runtime state. This is the point where you'll likely want to construct the client per-request with resolved credentials rather than resolving a singleton from the container. Confirm whether the package supports constructor injection of credentials; if not, this is your first fork candidate.

Gotchas and Tips

Verify the package's health before betting on it. 25 stars and zero dependents means nobody else is finding the bugs for you. There's no community to ask. Check the commit history and issue tracker directly: is the last release recent and does it match Salvador's current webservice version? Municipality integrations rot — endpoint URLs and required XML schema versions change, and an unmaintained wrapper can fail silently for months. Write your wrapper so a fork is a one-day job, not a rewrite.

Homologation is not a sandbox. Unlike payment gateways, some municipal test environments still issue documents with real numbering and real fiscal meaning, and some cities don't have a genuine test environment at all. Confirm the homologation endpoint's semantics before you run "just a quick test" with a production CNPJ. Never point a test suite at an endpoint you haven't confirmed is harmless.

RPS gaps and sequence resets. If you ever restore a database backup, truncate the invoice table, or migrate between environments, you can rewind the RPS sequence. Re-using a number produces a rejection or, worse, an inconsistency in the municipal records. Back up the sequence table with the same care as your financial data.

Encoding will bite you. Salvador's webservice has historically been picky about character encoding in string fields — razao social, endereço, discriminação do serviço. Accents in a UTF-8 document can arrive mangled if the service expects a different encoding, or if the XML declaration doesn't match the actual bytes. Sanitize free-text fields (strip control characters, normalize) and test with a customer whose name contains Ç, Ã, and an ampersand.

Money as strings, always. Never let a float reach the XML. Use number_format($value, 2, '.', '') and pass strings. A single float-rounding artifact in an ISS base calculation is a fiscal discrepancy you'll be explaining to an auditor.

XML signature canonicalization. If you ever hand-build or post-process XML — for cancelation, or to add a field the wrapper doesn't expose — the signature is computed over a specific canonical form. Reformatting, re-indenting, or reordering elements after signing invalidates it, and the municipality's error message will not say "your signature is broken." It'll say something generic. Use the package's signing path; don't hand-roll it.

Cancelation has a legal time window. Salvador, like most municipalities, restricts cancelation by elapsed time and by whether the tomador has already used the invoice. Model cancelation as a first-class domain operation with its own guard clauses and audit trail, not a button that calls a method. Also confirm whether you need a justification text with a minimum length — it's a common last-minute rejection.

Error messages are cryptic and sometimes wrong. A rejection that says "serviço inválido" may actually mean your certificate's CNPJ doesn't match the inscrição municipal in config. Standard practice: log the full raw response, keep a growing internal mapping of municipality error codes to actual causes, and surface the raw text to operators alongside your interpretation.

Timezone. Brazil has multiple offsets and has historically tinkered with DST. Set America/Bahia (or America/Sao_Paulo, they're currently equivalent) explicitly in config/app.php and in the package config if it exposes a date/timezone setting. An emission timestamp off by three hours can invalidate a competency-period check.

Extension points to probe in the source. Look for: whether the client is bound as a singleton (affects multi-tenant), whether the signing step is its own class (the first thing you'll want to override), whether endpoints are config-driven (needed for homologation switching), and whether responses are returned as raw strings or parsed objects (determines how much parsing you write yourself). Anything not exposed as a seam — make it a fork.

Plan the fork. With zero dependents, you are effectively the maintainer. Fork the repo into your own namespace early, keep the original as an upstream remote, and cherry-pick if it ever gets fixed. It's a 10-minute task that saves you from a hard blocker when Salvador changes its schema and upstream doesn't respond.

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels