n1ebieski/ksef-php-client

PHP API client that allows you to interact with the API Krajowego Systemu e-Faktur

Installs: 97

Dependents: 0

Suggesters: 0

Security: 0

Stars: 7

Watchers: 3

Forks: 1

Open Issues: 0

pkg:composer/n1ebieski/ksef-php-client


README

1920x810

KSEF PHP Client

This package is not production ready yet!

PHP API client that allows you to interact with the KSEF API Krajowy System e-Faktur

Main features:

  • Support for authorization using qualified certificates, KSeF certificates, KSeF tokens, and trusted ePUAP signatures (manual mode)
  • Logical invoice structure mapped to DTOs and Value Objects
  • Automatic access token refresh
  • CSR (Certificate Signing Request) handling
  • KSeF exception handling
  • QR codes generation
KSEF Version Branch Release Version
2.0 main ^0.3
1.0 0.2.x 0.2.*

Table of Contents

Get Started

Requires PHP 8.1+

First, install ksef-php-client via the Composer package manager:

composer require n1ebieski/ksef-php-client

Ensure that the php-http/discovery composer plugin is allowed to run or install a client manually if your project does not already have a PSR-18 client integrated.

composer require guzzlehttp/guzzle

Client configuration

use N1ebieski\KSEFClient\ClientBuilder;
use N1ebieski\KSEFClient\ValueObjects\Mode;
use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory;

$client = (new ClientBuilder())
    ->withMode(Mode::Production) // Choice between: Test, Demo, Production
    ->withApiUrl($_ENV['KSEF_API_URL']) // Optional, default is set by Mode selection
    ->withHttpClient(new \GuzzleHttp\Client(...)) // Optional PSR-18 implementation, default is set by Psr18ClientDiscovery::find()
    ->withLogger(new \Monolog\Logger(...)) // Optional PSR-3 implementation, default is set by PsrDiscovery\Discover::log()
    ->withLogPath($_ENV['PATH_TO_LOG_FILE'], $_ENV['LOG_LEVEL']) // Optional, level: null disables logging
    ->withAccessToken($_ENV['ACCESS_TOKEN']) // Optional, if present, auto authorization is skipped
    ->withRefreshToken($_ENV['REFRESH_TOKEN']) // Optional, if present, auto refresh access token is enabled
    ->withKsefToken($_ENV['KSEF_TOKEN']) // Required for API Token authorization. Optional otherwise
    ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE']) // Required .p12 file for Certificate authorization. Optional otherwise
    ->withVerifyCertificateChain(true) // Optional. Explanation https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1xades-signature/post
    ->withEncryptionKey(EncryptionKeyFactory::makeRandom()) // Required for invoice resources. Remember to save this value!
    ->withIdentifier('NIP_NUMBER') // Required for authorization. Optional otherwise
    ->build();

Auto mapping

Each resource supports mapping through both an array and a DTO, for example:

use N1ebieski\KSEFClient\Requests\Auth\Status\StatusRequest;
use N1ebieski\KSEFClient\Requests\ValueObjects\ReferenceNumber;

$authorisationStatusResponse = $client->auth()->status(new StatusRequest(
    referenceNumber: ReferenceNumber::from('20250508-EE-B395BBC9CD-A7DB4E6095-BD')
))->object();

or:

$authorisationStatusResponse = $client->auth()->status([
    'referenceNumber' => '20250508-EE-B395BBC9CD-A7DB4E6095-BD'
])->object();

Authorization

Auto authorization via KSEF Token

use N1ebieski\KSEFClient\ClientBuilder;

$client = (new ClientBuilder())
    ->withKsefToken($_ENV['KSEF_KEY'])
    ->withIdentifier('NIP_NUMBER')
    ->build();

// Do something with the available resources

Auto authorization via certificate .p12

use N1ebieski\KSEFClient\ClientBuilder;

$client = (new ClientBuilder())
    ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
    ->withIdentifier('NIP_NUMBER')
    ->build();

// Do something with the available resources

Manual authorization

use N1ebieski\KSEFClient\ClientBuilder;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Requests\Auth\DTOs\XadesSignature;
use N1ebieski\KSEFClient\Requests\Auth\XadesSignature\XadesSignatureXmlRequest;

$client = (new ClientBuilder())->build();

$nip = 'NIP_NUMBER';

$authorisationChallengeResponse = $client->auth()->challenge()->object();

$xml = XadesSignature::from([
    'challenge' => $authorisationChallengeResponse->challenge,
    'contextIdentifierGroup' => [
        'identifierGroup' => [
            'nip' => $nip
        ]
    ],
    'subjectIdentifierType' => 'certificateSubject'
])->toXml();

$signedXml = 'SIGNED_XML_DOCUMENT'; // Sign a xml document via Szafir, ePUAP etc.

$authorisationAccessResponse = $client->auth()->xadesSignature(
    new XadesSignatureXmlRequest($signedXml)
)->object();

$client = $client->withAccessToken($authorisationAccessResponse->authenticationToken->token);

$authorisationStatusResponse = Utility::retry(function () use ($client, $authorisationAccessResponse) {
    $authorisationStatusResponse = $client->auth()->status([
        'referenceNumber' => $authorisationAccessResponse->referenceNumber
    ])->object();

    if ($authorisationStatusResponse->status->code === 200) {
        return $authorisationStatusResponse;
    }

    if ($authorisationStatusResponse->status->code >= 400) {
        throw new RuntimeException(
            $authorisationStatusResponse->status->description,
            $authorisationStatusResponse->status->code
        );
    }
});

$authorisationTokenResponse = $client->auth()->token()->redeem()->object();

$client = $client
    ->withAccessToken(
        token: $authorisationTokenResponse->accessToken->token, 
        validUntil: $authorisationTokenResponse->accessToken->validUntil
    )
    ->withRefreshToken(
        token: $authorisationTokenResponse->refreshToken->token,
        validUntil: $authorisationTokenResponse->refreshToken->validUntil
    );

// Do something with the available resources

Resources

Auth

Challenge

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1challenge/post

$response = $client->auth()->challenge()->object();

Xades Signature

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1xades-signature/post

use N1ebieski\KSEFClient\Requests\Auth\XadesSignature\XadesSignatureRequest;

$response = $client->auth()->xadesSignature(
    new XadesSignatureRequest(...)
)->object();

or:

use N1ebieski\KSEFClient\Requests\Auth\XadesSignature\XadesSignatureXmlRequest;

$response = $client->auth()->xadesSignature(
    new XadesSignatureXmlRequest(...)
)->object();

Auth Status

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1%7BreferenceNumber%7D/get

use N1ebieski\KSEFClient\Requests\Auth\Status\StatusRequest;

$response = $client->auth()->status(
    new StatusRequest(...)
)->object();

Token

Redeem

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1token~1redeem/post

$response = $client->auth()->token()->redeem()->object();
Refresh

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Uzyskiwanie-dostepu/paths/~1api~1v2~1auth~1token~1refresh/post

$response = $client->auth()->token()->refresh()->object();

Security

Public Key Certificates

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty-klucza-publicznego/paths/~1api~1v2~1security~1public-key-certificates/get

$response = $client->security()->publicKeyCertificates()->object();

Sessions

Sessions Invoices

Upo

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D~1invoices~1%7BinvoiceReferenceNumber%7D~1upo/get

use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Upo\UpoRequest;

$response = $client->sessions()->invoices()->upo(
    new UpoRequest(...)
)->body();
Ksef Upo

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D~1invoices~1ksef~1%7BksefNumber%7D~1upo/get

use N1ebieski\KSEFClient\Requests\Sessions\Invoices\KsefUpo\KsefUpoRequest;

$response = $client->sessions()->invoices()->ksefUpo(
    new KsefUpoRequest(...)
)->body();
Invoices Status

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D~1invoices~1%7BinvoiceReferenceNumber%7D/get

use N1ebieski\KSEFClient\Requests\Sessions\Invoices\Status\StatusRequest;

$response = $client->sessions()->invoices()->status(
    new StatusRequest(...)
)->object();

Online

Open

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Wysylka-interaktywna/paths/~1api~1v2~1sessions~1online/post

use N1ebieski\KSEFClient\Requests\Sessions\Online\Open\OpenRequest;

$response = $client->sessions()->online()->open(
    new OpenRequest(...)
)->object();
Close

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Wysylka-interaktywna/paths/~1api~1v2~1sessions~1online~1%7BreferenceNumber%7D~1close/post

use N1ebieski\KSEFClient\Requests\Sessions\Online\Close\CloseRequest;

$response = $client->sessions()->online()->close(
    new CloseRequest(...)
)->status();
Invoices send

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Wysylka-interaktywna/paths/~1api~1v2~1sessions~1online~1%7BreferenceNumber%7D~1invoices/post

for DTO invoice:

use N1ebieski\KSEFClient\Requests\Sessions\Online\Send\SendRequest;

$response = $client->sessions()->online()->send(
    new SendRequest(...)
)->object();

for XML invoice:

use N1ebieski\KSEFClient\Requests\Sessions\Online\Send\SendXmlRequest;

$response = $client->sessions()->online()->send(
    new SendXmlRequest(...)
)->object();

Sessions Status

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Status-wysylki-i-UPO/paths/~1api~1v2~1sessions~1%7BreferenceNumber%7D/get

use N1ebieski\KSEFClient\Requests\Sessions\Status\StatusRequest;

$response = $client->sessions()->status(
    new StatusRequest(...)
)->object();

Invoices

Invoices Download

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Pobieranie-faktur/paths/~1api~1v2~1invoices~1ksef~1%7BksefNumber%7D/get

use N1ebieski\KSEFClient\Requests\Invoices\Download\DownloadRequest;

$response = $client->invoices()->download(
    new DownloadRequest(...)
)->body();

Query

Query Metadata

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Pobieranie-faktur/paths/~1api~1v2~1invoices~1query~1metadata/post

use N1ebieski\KSEFClient\Requests\Invoices\Query\Metadata\MetadataRequest;

$response = $client->invoices()->query()->metadata(
    new MetadataRequest(...)
)->object();

Exports

Exports Init

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Pobieranie-faktur/paths/~1api~1v2~1invoices~1exports/post

use N1ebieski\KSEFClient\Requests\Invoices\Exports\Init\InitRequest;

$response = $client->invoices()->exports()->init(
    new InitRequest(...)
)->object();
Exports Status

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Pobieranie-faktur/paths/~1api~1v2~1invoices~1exports~1%7BoperationReferenceNumber%7D/get

use N1ebieski\KSEFClient\Requests\Invoices\Exports\Status\StatusRequest;

$response = $client->invoices()->exports()->status(
    new StatusRequest(...)
)->object();

Certificates

Limits

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1limits/get

$response = $client->certificates()->limits()->object();

Enrollments

Enrollments Data

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1enrollments~1data/get

$response = $client->certificates()->enrollments()->data()->object();
Enrollments Send

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1enrollments/post

use N1ebieski\KSEFClient\Requests\Certificates\Enrollments\Send\SendRequest;

$response = $client->certificates()->enrollments()->send(
    new SendRequest(...)
)->object();
Enrollments Status

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1enrollments~1%7BreferenceNumber%7D/get

use N1ebieski\KSEFClient\Requests\Certificates\Enrollments\Status\StatusRequest;

$response = $client->certificates()->enrollments()->status(
    new StatusRequest(...)
)->object();

Certificates Retrieve

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1retrieve/post

use N1ebieski\KSEFClient\Requests\Certificates\Retrieve\RetrieveRequest;

$response = $client->certificates()->retrieve(
    new RetrieveRequest(...)
)->object();

Certificates Revoke

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1%7BcertificateSerialNumber%7D~1revoke/post

use N1ebieski\KSEFClient\Requests\Certificates\Revoke\RevokeRequest;

$response = $client->certificates()->revoke(
    new RevokeRequest(...)
)->status();

Certificates Query

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1query/post

use N1ebieski\KSEFClient\Requests\Certificates\Query\QueryRequest;

$response = $client->certificates()->query(
    new QueryRequest(...)
)->object();

Tokens

Tokens Create

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1api~1v2~1tokens/post

use N1ebieski\KSEFClient\Requests\Tokens\Create\CreateRequest;

$response = $client->tokens()->create(
    new CreateRequest(...)
)->object();

Tokens List

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1api~1v2~1tokens/get

use N1ebieski\KSEFClient\Requests\Tokens\List\ListRequest;

$response = $client->tokens()->list(
    new ListRequest(...)
)->object();

Tokens Status

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1api~1v2~1tokens~1%7BreferenceNumber%7D/get

use N1ebieski\KSEFClient\Requests\Tokens\Status\StatusRequest;

$response = $client->tokens()->list(
    new StatusRequest(...)
)->object();

Tokens Revoke

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Tokeny-KSeF/paths/~1api~1v2~1tokens~1%7BreferenceNumber%7D/delete

use N1ebieski\KSEFClient\Requests\Tokens\Revoke\RevokeRequest;

$response = $client->tokens()->revoke(
    new RevokeRequest(...)
)->status();

Testdata

Person

Person Create

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Dane-testowe/paths/~1api~1v2~1testdata~1person/post

use N1ebieski\KSEFClient\Requests\Testdata\Person\Create\CreateRequest;

$response = $client->testdata()->person()->create(
    new CreateRequest(...)
)->status();
Person Remove

https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Dane-testowe/paths/~1api~1v2~1testdata~1person~1remove/post

use N1ebieski\KSEFClient\Requests\Testdata\Person\Remove\RemoveRequest;

$response = $client->testdata()->person()->remove(
    new RemoveRequest(...)
)->status();

Examples

Generate a KSEF certificate and convert to .p12 file

<?php

use N1ebieski\KSEFClient\Actions\ConvertCertificateToPkcs12\ConvertCertificateToPkcs12Action;
use N1ebieski\KSEFClient\Actions\ConvertCertificateToPkcs12\ConvertCertificateToPkcs12Handler;
use N1ebieski\KSEFClient\Actions\ConvertDerToPem\ConvertDerToPemAction;
use N1ebieski\KSEFClient\Actions\ConvertDerToPem\ConvertDerToPemHandler;
use N1ebieski\KSEFClient\Actions\ConvertPemToDer\ConvertPemToDerAction;
use N1ebieski\KSEFClient\Actions\ConvertPemToDer\ConvertPemToDerHandler;
use N1ebieski\KSEFClient\ClientBuilder;
use N1ebieski\KSEFClient\DTOs\DN;
use N1ebieski\KSEFClient\Factories\CSRFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\ValueObjects\Certificate;
use N1ebieski\KSEFClient\ValueObjects\Mode;
use N1ebieski\KSEFClient\ValueObjects\PrivateKeyType;

$client = (new ClientBuilder())
    ->withMode(Mode::Test)
    ->withIdentifier('NIP_NUMBER')
    // To generate the KSEF certificate, you have to authorize the qualified certificate the first time
    ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
    ->build();

$dataResponse = $client->certificates()->enrollments()->data()->json();

$dn = DN::from($dataResponse);

// You can choose beetween EC or RSA private key type
$csr = CSRFactory::make($dn, PrivateKeyType::EC);

$csrToDer = (new ConvertPemToDerHandler())->handle(new ConvertPemToDerAction($csr->raw));

$sendResponse = $client->certificates()->enrollments()->send([
    'certificateName' => 'My first certificate',
    'certificateType' => 'Authentication',
    'csr' => base64_encode($csrToDer),
])->object();

$statusResponse = Utility::retry(function () use ($client, $sendResponse) {
    $statusResponse = $client->certificates()->enrollments()->status([
        'referenceNumber' => $sendResponse->referenceNumber
    ])->object();

    if ($statusResponse->status->code === 200) {
        return $statusResponse;
    }

    if ($statusResponse->status->code >= 400) {
        throw new RuntimeException(
            $statusResponse->status->description,
            $statusResponse->status->code
        );
    }
});

$retrieveResponse = $client->certificates()->retrieve([
    'certificateSerialNumbers' => [$statusResponse->certificateSerialNumber]
])->object();

$certificate = base64_decode($retrieveResponse->certificates[0]->certificate);

$certificateToPem = (new ConvertDerToPemHandler())->handle(
    new ConvertDerToPemAction($certificate, 'CERTIFICATE')
);

$certificateToPkcs12 = (new ConvertCertificateToPkcs12Handler())->handle(
    new ConvertCertificateToPkcs12Action(
        certificate: new Certificate($certificateToPem, [], $csr->privateKey),
        passphrase: 'password'
    )
);

file_put_contents(Utility::basePath('config/certificates/ksef-certificate.p12'), $certificateToPkcs12);

Send an invoice, check for UPO and generate QR code

<?php

use Endroid\QrCode\Builder\Builder as QrCodeBuilder;
use Endroid\QrCode\Label\Font\OpenSans;
use Endroid\QrCode\RoundBlockSizeMode;
use N1ebieski\KSEFClient\Actions\ConvertEcdsaDerToRaw\ConvertEcdsaDerToRawHandler;
use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesAction;
use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesHandler;
use N1ebieski\KSEFClient\ClientBuilder;
use N1ebieski\KSEFClient\DTOs\QRCodes;
use N1ebieski\KSEFClient\DTOs\Requests\Sessions\Online\Faktura;
use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Testing\Fixtures\Requests\Sessions\Online\Send\SendFakturaSprzedazyTowaruRequestFixture;
use N1ebieski\KSEFClient\ValueObjects\Mode;

$encryptionKey = EncryptionKeyFactory::makeRandom();

$client = (new ClientBuilder())
    ->withMode(Mode::Test)
    ->withIdentifier('NIP_NUMBER')
    ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
    ->withEncryptionKey($encryptionKey)
    ->build();

$openResponse = $client->sessions()->online()->open([
    'formCode' => 'FA (3)',
])->object();

$fixture = (new SendFakturaSprzedazyTowaruRequestFixture())
    ->withTodayDate()
    ->withRandomInvoiceNumber();

$sendResponse = $client->sessions()->online()->send([
    ...$fixture->data,
    'referenceNumber' => $openResponse->referenceNumber,
])->object();

$closeResponse = $client->sessions()->online()->close([
    'referenceNumber' => $openResponse->referenceNumber
]);

$statusResponse = Utility::retry(function () use ($client, $openResponse, $sendResponse) {
    $statusResponse = $client->sessions()->invoices()->status([
        'referenceNumber' => $openResponse->referenceNumber,
        'invoiceReferenceNumber' => $sendResponse->referenceNumber
    ])->object();

    if ($statusResponse->status->code === 200) {
        return $statusResponse;
    }

    if ($statusResponse->status->code >= 400) {
        throw new RuntimeException(
            $statusResponse->status->description,
            $statusResponse->status->code
        );
    }
});

$upo = $client->sessions()->invoices()->upo([
    'referenceNumber' => $openResponse->referenceNumber,
    'invoiceReferenceNumber' => $sendResponse->referenceNumber
])->body();

$faktura = Faktura::from($fixture->getFaktura());

$generateQRCodesHandler = new GenerateQRCodesHandler(
    qrCodeBuilder: (new QrCodeBuilder())
        ->roundBlockSizeMode(RoundBlockSizeMode::Enlarge)
        ->labelFont(new OpenSans(size: 12)),
    convertEcdsaDerToRawHandler: new ConvertEcdsaDerToRawHandler()
);

/** @var QRCodes $qrCodes */
$qrCodes = $generateQRCodesHandler->handle(GenerateQRCodesAction::from([
    'nip' => $faktura->podmiot1->daneIdentyfikacyjne->nip,
    'invoiceCreatedAt' => $faktura->fa->p_1->value,
    'document' => $faktura->toXml(),
    'ksefNumber' => $statusResponse->ksefNumber
]));

// Invoice link
file_put_contents(Utility::basePath("var/qr/code1.png"), $qrCodes->code1);

Create an offline invoice and generate both QR codes

<?php

use Endroid\QrCode\Builder\Builder as QrCodeBuilder;
use Endroid\QrCode\Label\Font\OpenSans;
use Endroid\QrCode\RoundBlockSizeMode;
use N1ebieski\KSEFClient\Actions\ConvertEcdsaDerToRaw\ConvertEcdsaDerToRawHandler;
use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesAction;
use N1ebieski\KSEFClient\Actions\GenerateQRCodes\GenerateQRCodesHandler;
use N1ebieski\KSEFClient\DTOs\QRCodes;
use N1ebieski\KSEFClient\DTOs\Requests\Sessions\Online\Faktura;
use N1ebieski\KSEFClient\Factories\CertificateFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\Testing\Fixtures\Requests\Sessions\Online\Send\SendFakturaSprzedazyTowaruRequestFixture;
use N1ebieski\KSEFClient\ValueObjects\CertificatePath;

$nip = 'NIP_NUMBER';

// From https://ksef-test.mf.gov.pl/docs/v2/index.html#tag/Certyfikaty/paths/~1api~1v2~1certificates~1query/post
$certificateSerialNumber = $_ENV['CERTIFICATE_SERIAL_NUMBER'];
// Remember: this certificate must be "Offline" type, not "Authentication"
$certificate = CertificateFactory::make(
    CertificatePath::from($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
);

$fixture = (new SendFakturaSprzedazyTowaruRequestFixture())
    ->withTodayDate()
    ->withRandomInvoiceNumber();

$faktura = Faktura::from($fixture->getFaktura());

$generateQRCodesHandler = new GenerateQRCodesHandler(
    qrCodeBuilder: (new QrCodeBuilder())
        ->roundBlockSizeMode(RoundBlockSizeMode::Enlarge)
        ->labelFont(new OpenSans(size: 12)),
    convertEcdsaDerToRawHandler: new ConvertEcdsaDerToRawHandler()
);

/** @var QRCodes $qrCodes */
$qrCodes = $generateQRCodesHandler->handle(GenerateQRCodesAction::from([
    'nip' => $faktura->podmiot1->daneIdentyfikacyjne->nip,
    'invoiceCreatedAt' => $faktura->fa->p_1->value,
    'document' => $faktura->toXml(),
    'certificate' => $certificate,
    'certificateSerialNumber' => $certificateSerialNumber,
    'contextIdentifierGroup' => [
        'identifierGroup' => [
            'nip' => $nip
        ]
    ]
]));

// Invoice link
file_put_contents(Utility::basePath("var/qr/code1.png"), $qrCodes->code1);

// Certificate verification link
file_put_contents(Utility::basePath("var/qr/code2.png"), $qrCodes->code2);

Download and decrypt invoices using the encryption key

<?php

use N1ebieski\KSEFClient\Actions\DecryptDocument\DecryptDocumentAction;
use N1ebieski\KSEFClient\Actions\DecryptDocument\DecryptDocumentHandler;
use N1ebieski\KSEFClient\ClientBuilder;
use N1ebieski\KSEFClient\Factories\EncryptionKeyFactory;
use N1ebieski\KSEFClient\Support\Utility;
use N1ebieski\KSEFClient\ValueObjects\Mode;

$encryptionKey = EncryptionKeyFactory::makeRandom();

$client = (new ClientBuilder())
    ->withMode(Mode::Test)
    ->withIdentifier($_ENV['NIP_NUMBER'])
    ->withCertificatePath($_ENV['PATH_TO_CERTIFICATE'], $_ENV['CERTIFICATE_PASSPHRASE'])
    ->withEncryptionKey($encryptionKey)
    ->build();

$initResponse = $client->invoices()->exports()->init([
    'filters' => [
        'subjectType' => 'Subject1',
        'dateRange' => [
            'dateType' => 'Invoicing',
            'from' => new DateTimeImmutable('-1 day'),
            'to' => new DateTimeImmutable()
        ],
    ]
])->object();

$statusResponse = Utility::retry(function () use ($client, $initResponse) {
    $statusResponse = $client->invoices()->exports()->status([
        'operationReferenceNumber' => $initResponse->operationReferenceNumber
    ])->object();

    if ($statusResponse->status->code === 200) {
        return $statusResponse;
    }

    if ($statusResponse->status->code >= 400) {
        throw new RuntimeException(
            $statusResponse->status->description,
            $statusResponse->status->code
        );
    }
});

$decryptDocumentHandler = new DecryptDocumentHandler();

// Downloading...
foreach ($statusResponse->package->parts as $part) {
    $contents = file_get_contents($part->url);

    $contents = $decryptDocumentHandler->handle(new DecryptDocumentAction(
        document: $contents,
        encryptionKey: $encryptionKey
    ));

    $name = rtrim($part->partName, '.aes');

    file_put_contents(Utility::basePath("var/zip/{$name}"), $contents);
}

Testing

The package uses unit tests via Pest.

Pest configuration is located in tests/Pest

TestCase is located in tests/AbstractTestCase

Fake request and responses fixtures for resources are located in src/Testing/Fixtures/Requests

Run all tests:

composer install
vendor/bin/pest

Roadmap

  1. Batch endpoints
  2. Prepare the package for release candidate

Special thanks

Special thanks to: