Search by

beziworld / laravel-edi-epp

kamcw

Read and write InsERT EDI++ (.epp) exchange files for Subiekt GT, Subiekt nexo, Rewizor and Rachmistrz from Laravel: typed builders, streaming reader, spec validation.

Package info

github.com/Beziworld/laravel-edi-epp

Homepage

Issues

pkg:composer/beziworld/laravel-edi-epp

Statistics

Installs: 1

Dependents: 0

Suggesters: 0

Stars: 0

v1.0.0 2026-11-04 20:05 UTC

This package is auto-updated.

Last update: 2026-11-04 20:08:04 UTC


README

tests Latest Version License

Read and write InsERT EDI++ (.epp) exchange files from Laravel. EDI++ is the text format that Subiekt GT, Subiekt nexo, Rachmistrz GT, Rewizor GT and other InsERT programs use to import and export contractors, products and documents. Hand your users a file they can import, or read the files their Subiekt exports.

  • writer with the exact file format: Windows-1250 (or 852) encoding, CRLF, [INFO], [NAGLOWEK] and [ZAWARTOSC] sections, quoted text with doubled quotes, decimals with a dot and four places, yyyymmddhhnnss dates
  • typed builders: contractors (KONTRAHENCI with groups, features, extras), products and services (TOWARY with prices, VAT rates, units, EAN, CN codes, GTU markers), sales and purchase documents with lines, corrections, VAT summary rows, cash and bank payments (KP, KW, BP, BW) and document-scoped sections
  • document set exporter: one file with the dictionaries followed by the documents; contractors and products used on documents are added to the dictionaries automatically
  • line values, VAT summary per rate and totals calculated exactly (net or gross priced documents, corrections)
  • streaming reader into typed DTOs with conversion to UTF-8, robust to unknown sections, keywords and document types (kept verbatim) and to field count differences (reported as issues, or thrown in strict mode)
  • validation against the specification: record layouts per version (1.05 to 1.11), field types, enumerations, text lengths, sections and document types not available in the selected version
  • download responses with the right charset, epp:inspect Artisan command

Polska wersja dokumentacji

Why this package

Packagist has no EDI++ library: searching for "epp" returns Extensible Provisioning Protocol clients for domain registries, "edi++" returns UN/EDIFACT and X12 tools, and the Subiekt packages (lukeotdr/api-subiekt-gt, asocial-media/subiekt-sfera) wrap the Sfera COM API, which needs a Windows machine next to the client's database and a separate licence. The open-source EPP tools on GitHub are one-way converters in C#, Python, Go or Java (CSV/XLSX/XML to EPP, EPP to JSON). This package is a PHP reader and writer with the record layouts of every public specification version, validation and Laravel integration, so a shop, SaaS or ERP connector can exchange data with InsERT programs through plain files.

Specification coverage

Layouts were transcribed from the public InsERT documents "Komunikacja - Struktura danych EDI++" for versions 1.05.1, 1.05.3, 1.09 and 1.11 (the newest public document). The writer supports versions 1.05, 1.05.1 to 1.05.5, 1.06, 1.07, 1.08, 1.09, 1.10 and 1.11 (default). Sections introduced in newer versions (for example DOKUMENTYKSEF from 1.12) are not documented publicly; the reader keeps them as UnknownSection objects.

Area Sections
File header [INFO] (24 fields)
Trade and warehouse documents FS, FZ, PA, RS, RZ, FR, KFS, KFZ, KRS, KRZ, ZK, ZD, WZ, PZ, VWZ, VPZ, PW, RW, MMW, ZW; FWN, FWO, KWN, KWO, FM, KFM (accounting office)
Content lines (22 fields, 31 on corrections) or VAT summary rows (5, 14 or 18 fields)
Payments KP, KW, BP, BW (56 fields)
Accounting office settlements RLS, RLZ; fiscal reports RF, PRF, NZ
Dictionaries KONTRAHENCI, GRUPYKONTRAHENTOW, CECHYKONTRAHENTOW, DODATKOWEKONTRAHENTOW, TOWARY, CENNIK, GRUPYTOWAROW, CECHYTOWAROW, DODATKOWETOWAROW, TOWARYKODYCN, TOWARYGRUPYJPKVAT, PRACOWNICY, URZEDY
Document-scoped DATYZAKONCZENIA, WYMAGALNOSCMPP, PRZYCZYNYKOREKT, NUMERYIDENTYFIKACYJNENABYWCOW, DOKUMENTYFISKALNEVAT, OPLATYDODATKOWE, OPLATYSPECJALNE, IDENTYFIKATORYPLATNOSCI, DOKUMENTYZNACZNIKIJPKVAT, DOSTAWYMARZOWEDLAFM, OPLATACUKROWA, SPECYFIKACJATOWAROWAWSTO, PLATNOSCI, INFORMACJEWSTO, DATYUJECIAKOREKT, STAWKIVATZAGRANICZNE

Installation

composer require beziworld/laravel-edi-epp
php artisan vendor:publish --tag=edi-epp-config   # optional

Requires PHP 8.3+ with ext-iconv and ext-mbstring, Laravel 12 or 13. Optional .env settings:

EDI_EPP_VERSION=1.11          # 1.05 ... 1.11
EDI_EPP_PURPOSE=3             # 0 accounting office (Rachmistrz/Rewizor), 3 other (Subiekt)
EDI_EPP_CODE_PAGE=1250        # 1250 or 852
EDI_EPP_TIMEZONE=Europe/Warsaw
EDI_EPP_PROGRAM="My Shop"

EDI_EPP_SENDER_CODE=SHOP
EDI_EPP_SENDER_NAME="My Shop Spółka z o.o."
EDI_EPP_SENDER_NIP=1234563218
EDI_EPP_SENDER_STREET="ul. Prosta 2"
EDI_EPP_SENDER_POST_CODE=30-001
EDI_EPP_SENDER_CITY=Kraków
EDI_EPP_WAREHOUSE_CODE=MAG

EDI_EPP_STRICT_ENCODING=true  # throw on characters missing in the code page
EDI_EPP_TRUNCATE_TEXT=false   # cut texts longer than the specification allows
EDI_EPP_STRICT_READING=false  # throw on the first problem while reading

Exporting documents for Subiekt

use Beziworld\EdiEpp\Builders\CashDocumentBuilder;
use Beziworld\EdiEpp\Builders\ContractorBuilder;
use Beziworld\EdiEpp\Builders\DocumentBuilder;
use Beziworld\EdiEpp\Builders\LineBuilder;
use Beziworld\EdiEpp\Builders\ProductBuilder;
use Beziworld\EdiEpp\Data\VatRate;
use Beziworld\EdiEpp\Facades\EdiEpp;

$customer = ContractorBuilder::make('ACME')
    ->name('ACME Spółka z o.o.', 'ACME')
    ->nip('5261040828')
    ->address('ul. Długa 1', '00-001', 'Warszawa', 'Polska')
    ->email('biuro@acme.pl')
    ->group('Hurt');

$chalk = ProductBuilder::goods('KREDA-01', 'Kreda szkolna żółta')
    ->ean('5901234123457')
    ->unit('opak.')
    ->vat(23)                                  // or VatRate::exempt(), VatRate::reverseCharge(), ...
    ->price('Detaliczna', net: '5.00')         // gross 6.15 is calculated
    ->price('Hurtowa', net: '4.80')
    ->cn('25090000')
    ->gtu(6);

$invoice = DocumentBuilder::salesInvoice('FS 15/2026')
    ->issueDate('2026-11-04')
    ->saleDate('2026-11-03')
    ->contractor($customer)                    // header data copied, contractor added to KONTRAHENCI
    ->payment('Przelew', dueDate: '2026-11-18')
    ->line(LineBuilder::for($chalk, 20))       // code, unit, VAT and first price taken from the product
    ->line(LineBuilder::oneOffService('Transport', 1, '10.00')->vat(8))
    ->completionDate('2026-11-03')             // DATYZAKONCZENIA
    ->splitPayment(false);                     // WYMAGALNOSCMPP

$payment = CashDocumentBuilder::bankReceipt('BP 5/2026', '133.80')
    ->issueDate('2026-11-10')
    ->contractor($customer)
    ->settles($invoice)                        // title = "FS 15/2026"
    ->bank('mBank', '11 1140 2004 0000 3002 0135 5387');

$export = EdiEpp::export()                     // version, purpose, sender, warehouse from config
    ->createdBy('Jan Kowalski')
    ->documents([$invoice, $payment]);

return EdiEpp::download($export, 'sprzedaz-listopad.epp');   // or EdiEpp::write($export), EdiEpp::save($export, $path)

Missing line values (net and gross price, net, VAT and gross value), the VAT summary per rate, the header totals, the line count and the amount due are calculated with exact decimal arithmetic. Documents priced net (default) calculate VAT from the sum of net values per rate; ->grossPrices() calculates it from the gross sum. Every value can also be set explicitly, and any field of any record can be set by its key with ->set('fieldKey', $value).

Corrections

$correction = DocumentBuilder::salesCorrection('KFS 2/2026', 'FS 15/2026', '2026-11-04')
    ->issueDate('2026-11-06')
    ->contractor($customer)
    ->line(LineBuilder::for($chalk, 15, '5.00')->before(20))   // 20 before, 15 after
    ->correctionReason('Zwrot części towaru', 1);

// header totals: -25.00 net, -5.75 VAT, -30.75 gross (after minus before)

Files for Rachmistrz and Rewizor

With purpose 0 (accounting office) trade documents carry VAT summary rows instead of lines. The rows are calculated from the lines or set explicitly:

use Beziworld\EdiEpp\Builders\FileBuilder;
use Beziworld\EdiEpp\Enums\Version;

$file = FileBuilder::make(Version::V1_11)
    ->forAccountingOffice()
    ->period('2026-10-01', '2026-10-31')
    ->document(DocumentBuilder::purchaseInvoice('FZ 9/2026', supplierNumber: 'F/2026/10/77')
        ->issueDate('2026-10-15')
        ->contractor('HURTOWNIA')
        ->vatRow(23, net: '900.00', vat: '207.00', gross: '1107.00')
        ->vatRow(VatRate::exempt(), '200.00', '0.00', '200.00')
        ->jpkMarkers(['mpp' => true, 'gtu06' => true]))    // DOKUMENTYZNACZNIKIJPKVAT
    ->build();

The VAT row layout follows the version: 5 fields in 1.05, 14 in 1.05.1, 18 from 1.05.3.

Products only (Subiekt nexo product import)

$file = EdiEpp::export()
    ->products(Product::query()->cursor()->map(fn ($product) => ProductBuilder::goods($product->sku, $product->name)
        ->ean($product->ean)
        ->vat($product->vat_rate)
        ->price('Detaliczna', gross: $product->price)))
    ->build();

InsERT notes that Subiekt nexo limits product symbols to 20 characters; the validator enforces the 20-character code of the specification.

Reading files

$file = EdiEpp::read(storage_path('import/export.epp'));   // path or stream; EdiEpp::parse($contents) for strings

$file->info->version();         // "1.11"
$file->info->program();         // "Subiekt GT"
$file->info->senderName();      // UTF-8, converted from code page 1250 or 852

foreach ($file->documents as $document) {
    $document->type();          // DocumentType::SalesInvoice
    $document->fullNumber();    // "FS 15/2026"
    $document->issueDate();     // CarbonImmutable (Europe/Warsaw)
    $document->contractorName();
    $document->grossValue();    // "133.8000" (decimal string)
    $document->completionDate();// linked from DATYZAKONCZENIA

    foreach ($document->lines as $line) {
        $line->productCode();   // "KREDA-01"
        $line->quantity();      // "20.0000"
        $line->netValue();
        $line->decimal('quantityBefore');   // correction data
    }

    $document->vatRows;         // accounting office files
}

$file->cashDocuments;           // KP, KW, BP, BW
$file->contractor('ACME')?->groups();
$file->product('KREDA-01')?->price('Hurtowa')?->net();
$file->records;                 // PRACOWNICY, URZEDY, ... and records without an owner
$file->unknownSections;         // sections of newer versions, kept verbatim
$file->issues;                  // field count or value problems, with line numbers

Every DTO keeps the normalised values in $fields (accessible with get(), text(), integer(), decimal(), flag(), date()) and the values as read in $raw. Large files can be streamed without linking:

foreach (EdiEpp::stream($path, fn (Issue $issue) => logger()->warning((string) $issue)) as $item) {
    // Info, Contractor, Product, Price, Document, CashDocument, Record, UnknownSection
}

Files declaring code page 1250 but saved as UTF-8 by other tools (or starting with a UTF-8 BOM) are detected and read correctly. Pass EdiEpp::reader(strict: true) to throw a ParseException on the first problem.

Validation

$violations = EdiEpp::validate($export);   // list of Violation; empty means the file can be written

foreach ($violations as $violation) {
    echo $violation;   // 'TOWARY KREDA-01 field "name": Text is longer than 50 characters.'
}

The writer validates before writing and throws InvalidRecord with all violations. Checked: known version, field types, enumerated values (document types, statuses, contractor and product types, VAT symbols, code page, ...), maximum text lengths, fields and sections newer than the file version, document types reserved for accounting office files, required identifying fields (codes, full numbers, issue dates) and unique codes.

Artisan

php artisan epp:inspect storage/import/export.epp
php artisan epp:inspect export.epp --json --limit=100
php artisan epp:inspect export.epp --strict

Prints the header, the number of contractors, products and documents per type, a document table and the issues.

Errors

Exception When
InvalidRecord data violates the specification; $violations lists every problem
EncodingException a character does not exist in the code page (strict encoding), unsupported code page
ParseException missing [INFO], unreadable file, any problem in strict reading mode ($issue)
UnsupportedVersion unknown version literal, a section newer than the file version
InvalidValue a value cannot be converted to the field type, unknown field key
InvalidConfiguration invalid version, purpose, code page or time zone in the configuration

All exceptions extend Beziworld\EdiEpp\Exceptions\EdiEppException.

Notes and limits

  • The public documents contain no example records; tests use fixtures written from the field tables and round trips.
  • Fields whose introduction version could not be confirmed: the ISO country code of contractors (written from 1.09), the JPK document type marker (from 1.08). Rewizor and Rachmistrz layouts from 1.06 onward are written with 18-field VAT rows.
  • Text with line breaks is written on one line (breaks replaced with spaces); the reader accepts quoted values that span lines.
  • Unknown sections are kept when reading but are not written back.
  • Importing documents (as opposed to products) from EPP into Subiekt nexo is not described in public InsERT materials; test with your target program.

Development

composer install
vendor/bin/pint
vendor/bin/phpstan analyse --memory-limit=1G
vendor/bin/pest

Author

Created and maintained by Kamil Ćwiertnia, beziworld.eu.

Need an integration? Get in touch through beziworld.eu.

License

MIT. See LICENSE.md. InsERT, Subiekt, Rachmistrz, Rewizor and EDI++ are names of InsERT S.A. This package is not affiliated with or endorsed by InsERT S.A.