This commit is contained in:
2026-04-15 01:24:42 +02:00
parent 6786665cbf
commit 596b7a995e
15 changed files with 1206 additions and 115 deletions

View File

@@ -8,6 +8,7 @@ class FakturowniaInvoiceImporter
private $apiClient;
private $startDate;
private $pageLimit = 100;
private $ownTaxNo = '';
public function __construct( $mdb = null )
{
@@ -37,6 +38,7 @@ class FakturowniaInvoiceImporter
$config['page_limit']
);
$this -> pageLimit = (int)$config['page_limit'];
$this -> ownTaxNo = (string)$config['own_tax_no'];
$summary = [
'imported' => 0,
@@ -85,13 +87,8 @@ class FakturowniaInvoiceImporter
if ( !is_array( $documents ) || empty( $documents ) )
break;
$hasRelevantDateInPage = false;
foreach ( $documents as $document )
{
if ( $this -> isDateRelevantForImport( $document ) )
$hasRelevantDateInPage = true;
$result = $this -> processSingleDocument( $document, $documentType );
$summary[ $result ]++;
}
@@ -99,11 +96,9 @@ class FakturowniaInvoiceImporter
if ( count( $documents ) < $this -> pageLimit )
break;
// API zwraca dokumenty malejaco po czasie. Gdy cala strona jest starsza niz startDate,
// kolejne strony tez beda starsze i nie ma sensu pobierac dalej.
if ( !$hasRelevantDateInPage )
break;
// Nie przerywamy petli na podstawie dat w liscie — API moze sortowac po updated_at/created_at
// zamiast issue_date, co powodowalo gubienie faktur z wcześniejsza data wystawienia.
// Filtr "za stare" dziala per-dokument w processSingleDocument (strtotime(date) < startDate).
$page++;
if ( $page > 100 )
break;
@@ -139,18 +134,39 @@ class FakturowniaInvoiceImporter
if ( !$clientMap )
{
$positionNames = [];
foreach ( $document['positions'] as $pos )
{
$name = trim( (string)( $pos['name'] ?? '' ) );
if ( $name !== '' )
$positionNames[] = $name;
}
$this -> repo -> queueUnmapped( 'client', $document['client_key'], $document['client_name'], [
'document_id' => $document['external_id'],
'document_number' => $document['number'],
'document_type' => $documentType,
'tax_no' => $document['client_tax_no']
'tax_no' => $document['client_tax_no'],
'positions' => $positionNames
] );
return 'unmapped';
}
$resolvedPositions = [];
$skippedPositions = [];
foreach ( $document['positions'] as $position )
{
// Per-dokument skip: uzytkownik oznaczyl konkretna pozycje na konkretnej fakturze jako pomijana.
// Pomijanie dzieje sie ZANIM sprawdzamy mapowanie, zeby pominieta pozycja nie blokowala importu.
if ( $this -> repo -> isDocumentPositionSkipped( $document['external_id'], $documentType, $position['item_key'] ) )
{
$skippedPositions[] = [
'item_key' => $position['item_key'],
'name' => $position['name']
];
continue;
}
$itemMap = $this -> repo -> getItemMapping( $position['item_key'] );
if ( !$itemMap )
{
@@ -159,7 +175,8 @@ class FakturowniaInvoiceImporter
'document_number' => $document['number'],
'document_type' => $documentType,
'buyer_name' => $document['buyer_name'],
'seller_name' => $document['seller_name']
'seller_name' => $document['seller_name'],
'position_name' => $position['name']
] );
return 'unmapped';
}
@@ -175,7 +192,9 @@ class FakturowniaInvoiceImporter
];
}
if ( empty( $resolvedPositions ) )
// Dokument bez zadnych pozycji do przetworzenia (ani resolved, ani skip) → nic nie zapisujemy.
// Nie oznaczamy jako imported, zeby nie zablokowal sie na zawsze.
if ( empty( $resolvedPositions ) && empty( $skippedPositions ) )
return 'skipped';
$operationIds = [];
@@ -206,7 +225,8 @@ class FakturowniaInvoiceImporter
$operationIds,
[
'number' => $document['number'],
'client_name' => $document['client_name']
'client_name' => $document['client_name'],
'skipped_positions' => $skippedPositions
]
);
@@ -261,22 +281,6 @@ class FakturowniaInvoiceImporter
return false;
}
private function isDateRelevantForImport( $rawDocument )
{
if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
$rawDocument = $rawDocument['invoice'];
if ( !is_array( $rawDocument ) )
return false;
$date = (string)( $rawDocument['issue_date'] ?? $rawDocument['sell_date'] ?? $rawDocument['created_at'] ?? '' );
$date = substr( $date, 0, 10 );
if ( !$date )
return false;
return strtotime( $date ) >= strtotime( $this -> startDate );
}
private function normalizeDocument( $rawDocument, $documentType )
{
if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
@@ -300,8 +304,22 @@ class FakturowniaInvoiceImporter
}
else
{
$clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
$clientTaxNo = (string)( $rawDocument['seller_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
// Dla kosztow kontrahentem jest "druga strona" — ta, ktorej NIP != naszego.
// Fakturownia dla wydatkow pobranych z KSeF (approval_status='received') odwraca role:
// seller_* to dane wlasnej firmy, buyer_* to prawdziwy dostawca. Bez tej heurystyki
// koszty z KSeF trafialyby do kolejki unmapped z wlasnym NIP-em jako klucz klienta.
$sellerTaxNo = (string)( $rawDocument['seller_tax_no'] ?? '' );
if ( $this -> taxNoEqualsOwn( $sellerTaxNo ) )
{
$clientName = (string)( $rawDocument['buyer_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
$clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
}
else
{
$clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
$clientTaxNo = $sellerTaxNo !== '' ? $sellerTaxNo : (string)( $rawDocument['tax_no'] ?? '' );
}
}
$positions = [];
@@ -436,6 +454,18 @@ class FakturowniaInvoiceImporter
return $normalizedTaxNo;
}
private function taxNoEqualsOwn( $taxNo )
{
if ( $this -> ownTaxNo === '' )
return false;
$normalized = $this -> normalizeTaxNo( $taxNo );
if ( $normalized === '' )
return false;
return $normalized === $this -> normalizeTaxNo( $this -> ownTaxNo );
}
private function buildItemKey( $rawPosition, $name )
{
if ( isset( $rawPosition['product_id'] ) && (string)$rawPosition['product_id'] !== '' )
@@ -483,6 +513,7 @@ class FakturowniaInvoiceImporter
$token = trim( (string)\Env::get( 'FAKTUROWNIA_API_TOKEN', '' ) );
$startDate = trim( (string)\Env::get( 'FAKTUROWNIA_START_DATE', '' ) );
$pageLimit = (int)\Env::get( 'FAKTUROWNIA_PAGE_LIMIT', 100 );
$ownTaxNo = trim( (string)\Env::get( 'FAKTUROWNIA_OWN_TAX_NO', '' ) );
if ( $domain === '' || $token === '' || $startDate === '' )
return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: brak konfiguracji w .env.' ];
@@ -498,7 +529,8 @@ class FakturowniaInvoiceImporter
'api_url' => rtrim( $domain, '/' ),
'token' => $token,
'start_date' => $startDate,
'page_limit' => $pageLimit > 0 ? $pageLimit : 100
'page_limit' => $pageLimit > 0 ? $pageLimit : 100,
'own_tax_no' => $ownTaxNo
];
}