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

Filament Vat Id Laravel Package

asignua/filament-vat-id

A Filament form field for collecting and validating VAT IDs in Laravel admin panels. It streamlines country-aware VAT input and validation so forms can verify business tax numbers with minimal setup.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and register. composer require asignua/filament-vat-id, then follow the README install block for your panel provider. The package is a Filament 5 form-field extension, so registration is either a plugin entry in the panel provider or nothing at all if you're only using the field class directly — confirm which applies to your version before adding boilerplate you don't need. If it publishes config or translation files, publish them now: the validation strings live there, and you want them in your repo, not in vendor/.

Pin the Filament major. The package targets Filament 5 specifically. Tighten the constraint in composer.json and treat a Filament major bump as a deliberate upgrade task, not something a composer update should decide for you. The field compensates for Filament's internal label-casing behavior, which is a convention rather than a public contract.

Where to look first.

  1. The README and the art/ screenshots — they define the expected rendering and label output.
  2. The workbench demo (DemoSeeder, "Acme Supply" data). This is the fastest way to see a working form without standing up your own panel, and it doubles as the fixture set for your own tests.
  3. The validation rule classes themselves (EU VAT, PL NIP/REGON, CZ IČO/DIČ, UA ЄДРПОУ/РНОКПП) — read what they actually accept before you promise a country's format is covered.

First use case. Drop the field into a billing-address form on a CompanyResource (or a customer onboarding wizard), right after the country select:

use Asignua\FilamentVatId\Forms\Components\TaxIdInput;

TaxIdInput::make('vat_id')
    ->label('VAT / Tax ID')
    ->required()
    ->columnSpan(1),

Then immediately run one real identifier per country you actually serve through the form — one valid, one deliberately broken checksum. Offline checksum validation either passes or fails deterministically, so this five-minute smoke test tells you more than the screenshots.

Implementation Patterns

Treat it as an ordinary Filament field. Everything you normally do composes on top: ->required(), ->live(onBlur: true), ->columnSpan(), ->helperText(), ->rules([...]), ->unique(), ->dehydrateStateUsing(). Don't wrap it in a custom component to "add validation" — that's what the field already is. Add your own rules on top only for things the package doesn't cover (e.g. "required when country is in the EU").

Country-driven conditional flow. The realistic pattern is a Select::make('country')->live() followed by the tax-ID field that keys off it:

Select::make('country')
    ->options(Country::pluck('name', 'iso2'))
    ->live()
    ->required(),

TaxIdInput::make('vat_id')
    ->required(fn (Get $get) => in_array($get('country'), ['PL', 'CZ', 'UA', 'DE', /* ... */]))
    ->visible(fn (Get $get) => $get('country') !== null)
    ->afterStateUpdated(fn () => null),

Keep B2C customers in mind — ->nullable() with conditional ->required() is the common shape, because sole traders and consumers legitimately don't have one. Validate on blur or on submit rather than on every keystroke if any lookup path is involved.

Normalize before you persist. The field is an input concern; your data model is a separate concern. Users paste PL 123-456-32-18, pl1234563218, and 1234563218. Decide on a canonical stored form (uppercase, country prefix, no separators) and apply it at the boundary:

TaxIdInput::make('vat_id')
    ->dehydrateStateUsing(fn (?string $state) => $state
        ? strtoupper(preg_replace('/[\s\-\.]/', '', $state))
        : null),

Store the canonical value for lookups and dedupe; keep the raw input only if you have an audit or support requirement. Do the same normalization in FormRequest rules if the identifier also arrives via API or import — don't assume every write path goes through Filament.

Split the two validation modes deliberately. Offline format/checksum validation belongs on the save path: it's fast, deterministic, and can't fail because VIES is down. Remote lookups (VIES, ARES, the Polish white list, GUS) belong on an explicit user action — an inline "Look up company" button or an action on the field — that pre-fills name, address, and registration status. Running a network call on submit turns a third-party outage into "customers can't check out." Cache lookup results keyed by normalized identifier; tax registration status doesn't change minute to minute.

Prefill from lookups, then let the human correct it. When a lookup returns a company name and address, populate the adjacent fields but leave them editable — registries lag behind renames and address changes. Show a subtle indicator of provenance ("verified via VIES, 2026-10-20") rather than silently trusting it.

Translations and copy. Keep overrides in lang/vendor/... in your app, never in vendor/. For a single field where product copy must be exact, ->validationAttribute('VAT / Tax ID') is the surgical tool and always wins. For panel-wide wording, use translations. The package ships 10 languages — verify yours is one of them before promising localized error messages; if it isn't, that's a translation file you own.

Test the field like any other form input. Use the workbench DemoSeeder data as fixtures and assert both behavior and message text:

Livewire::test(CompanyForm::class)
    ->fillForm(['country' => 'PL', 'vat_id' => '1234563218'])
    ->call('save')
    ->assertHasErrors(['vat_id']);

Build a fixture array of known-valid and known-invalid numbers per country from the registries' own published examples. This is cheap, catches checksum regressions on package updates, and catches the label-casing class of bug without a manual click-through.

Gotchas and Tips

lcfirst and acronyms. Filament lowercases the first character of derived labels when composing validation messages, which historically produced "The vAT / Tax ID is not…". v1.0.2 fixes this for TaxIdInput's own message only — other fields in your panel still get lcfirst. Don't generalize the fix. If you see "vAT" elsewhere, that's your label, not the package.

Message precedence, in order. Explicit ->validationAttribute() → package default → Filament-derived label → translation file. The first non-null wins. When a message looks wrong, walk that chain before filing a bug; a stray validationAttribute or a leftover lang/vendor override is the usual culprit.

Delete your old casing workaround. If you patched around "vAT" with a translation key, remove it on upgrade — it will shadow the corrected default and you'll spend an hour wondering why the fix "didn't work." v1.0.2 is docs plus this fix, no API changes, so the upgrade itself should be a no-op otherwise.

Checksum ≠ existence. Offline rules prove an identifier is well-formed and internally consistent. They do not prove the company exists, is VAT-registered, or is active. If your business logic depends on any of that (reverse-charge VAT, invoice issuance), the checksum is a gate, not an answer — you need the registry lookup and you need to record when it happened.

Remote lookups are a runtime dependency you're opting into. Budget for latency, rate limits, and scheduled maintenance windows (VIES has them). Never block a save on a lookup result; degrade to "unverified" and let the record persist. Wrap lookups in try/catch with a timeout, log failures, and surface a retry affordance rather than a validation error the user can't fix.

Format expectations vary by country prefix. EU VAT numbers are prefixed; national identifiers (NIP, IČO, ЄДРПОУ) often aren't. If your form collects both, be explicit about which field expects what, and normalize the prefix consistently — a mixed corpus of prefixed and unprefixed values makes dedupe and lookup unreliable later.

Empty string vs null. Filament will happily submit ''. If the column is nullable with a unique index, normalize '' to null in dehydrateStateUsing, or you'll get one empty-string row that blocks every subsequent customer without a VAT ID.

Don't subclass the field. Put normalization, cross-field rules, and registry side effects in your form/request/action layer. The code surface here is small and the release history is patch-level; keeping your logic outside means minor releases stay drop-in.

Read the rules before you rely on coverage. With a single-star package, you are the integration test. Skim the validation classes for the countries you actually serve, check how the rules are sourced and whether there's a refresh path, and write one test per country. If a country you need isn't covered, add a plain Laravel rule alongside the field rather than filing an issue and waiting.

Debug against the seeder. The screenshots come from the workbench DemoSeeder. If your rendering doesn't match the docs, reproduce with that seeder first — a mismatch is usually a locale difference or stale seeder data, not a field defect. It also gives you a known-good dataset to reference in support requests.

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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib