update
This commit is contained in:
162
autoload/Domain/Finances/FakturowniaApiClient.php
Normal file
162
autoload/Domain/Finances/FakturowniaApiClient.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
namespace Domain\Finances;
|
||||
|
||||
class FakturowniaApiClient
|
||||
{
|
||||
private $baseUrl;
|
||||
private $apiToken;
|
||||
private $pageLimit;
|
||||
private $timeout;
|
||||
|
||||
public function __construct( $baseUrl, $apiToken, $pageLimit = 100, $timeout = 20 )
|
||||
{
|
||||
$this -> baseUrl = rtrim( (string)$baseUrl, '/' );
|
||||
$this -> apiToken = (string)$apiToken;
|
||||
$this -> pageLimit = max( 1, (int)$pageLimit );
|
||||
$this -> timeout = max( 5, (int)$timeout );
|
||||
}
|
||||
|
||||
public function fetchSalesDocuments( $startDate, $page = 1 )
|
||||
{
|
||||
$query = [
|
||||
'page' => (int)$page,
|
||||
'per_page' => $this -> pageLimit
|
||||
];
|
||||
|
||||
if ( $this -> canUseCurrentMonthPeriod( $startDate ) )
|
||||
$query['period'] = 'this_month';
|
||||
|
||||
return $this -> requestList( '/invoices.json', $query );
|
||||
}
|
||||
|
||||
public function fetchCostDocuments( $startDate, $page = 1 )
|
||||
{
|
||||
$queries = [
|
||||
[
|
||||
'page' => (int)$page,
|
||||
'per_page' => $this -> pageLimit,
|
||||
'income' => 'no'
|
||||
]
|
||||
];
|
||||
|
||||
if ( $this -> canUseCurrentMonthPeriod( $startDate ) )
|
||||
$queries[0]['period'] = 'this_month';
|
||||
|
||||
$paths = [ '/costs.json', '/expenses.json', '/invoices.json' ];
|
||||
|
||||
foreach ( $paths as $path )
|
||||
{
|
||||
foreach ( $queries as $query )
|
||||
{
|
||||
$response = $this -> requestList( $path, $query, true );
|
||||
if ( $response['ok'] )
|
||||
return $response['data'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException( 'Nie udalo sie pobrac faktur kosztowych z API Fakturowni.' );
|
||||
}
|
||||
|
||||
public function fetchInvoiceDetails( $invoiceId )
|
||||
{
|
||||
$invoiceId = (int)$invoiceId;
|
||||
if ( $invoiceId <= 0 )
|
||||
return null;
|
||||
|
||||
$result = $this -> request( '/invoices/' . $invoiceId . '.json', [] );
|
||||
if ( $result['http_code'] >= 400 )
|
||||
return null;
|
||||
|
||||
$data = json_decode( $result['body'], true );
|
||||
return is_array( $data ) ? $data : null;
|
||||
}
|
||||
|
||||
private function requestList( $path, $query, $softFail = false )
|
||||
{
|
||||
$result = $this -> request( $path, $query );
|
||||
|
||||
if ( $result['http_code'] >= 400 )
|
||||
{
|
||||
if ( $softFail )
|
||||
return [ 'ok' => false, 'data' => [] ];
|
||||
|
||||
throw new \RuntimeException( 'Blad API Fakturowni: HTTP ' . $result['http_code'] . ' dla ' . $path );
|
||||
}
|
||||
|
||||
$data = json_decode( $result['body'], true );
|
||||
if ( !is_array( $data ) )
|
||||
{
|
||||
if ( $softFail )
|
||||
return [ 'ok' => false, 'data' => [] ];
|
||||
|
||||
throw new \RuntimeException( 'API Fakturowni zwrocilo niepoprawny JSON.' );
|
||||
}
|
||||
|
||||
$list = $this -> extractList( $data );
|
||||
return $softFail ? [ 'ok' => true, 'data' => $list ] : $list;
|
||||
}
|
||||
|
||||
private function request( $path, $query )
|
||||
{
|
||||
$query['api_token'] = $this -> apiToken;
|
||||
$url = $this -> baseUrl . $path . '?' . http_build_query( $query );
|
||||
|
||||
$ch = curl_init( $url );
|
||||
curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
|
||||
curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, $this -> timeout );
|
||||
curl_setopt( $ch, CURLOPT_TIMEOUT, $this -> timeout );
|
||||
curl_setopt( $ch, CURLOPT_HTTPHEADER, [
|
||||
'Accept: application/json'
|
||||
] );
|
||||
|
||||
$body = curl_exec( $ch );
|
||||
$httpCode = (int)curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$error = curl_error( $ch );
|
||||
curl_close( $ch );
|
||||
|
||||
if ( $body === false )
|
||||
throw new \RuntimeException( 'Blad polaczenia z API Fakturowni: ' . $error );
|
||||
|
||||
return [
|
||||
'http_code' => $httpCode,
|
||||
'body' => $body
|
||||
];
|
||||
}
|
||||
|
||||
private function extractList( $data )
|
||||
{
|
||||
if ( $this -> isList( $data ) )
|
||||
return $data;
|
||||
|
||||
$keys = [ 'invoices', 'costs', 'expenses', 'data' ];
|
||||
foreach ( $keys as $key )
|
||||
{
|
||||
if ( isset( $data[ $key ] ) && is_array( $data[ $key ] ) )
|
||||
{
|
||||
if ( $this -> isList( $data[ $key ] ) )
|
||||
return $data[ $key ];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function isList( $value )
|
||||
{
|
||||
if ( !is_array( $value ) )
|
||||
return false;
|
||||
|
||||
if ( $value === [] )
|
||||
return true;
|
||||
|
||||
return array_keys( $value ) === range( 0, count( $value ) - 1 );
|
||||
}
|
||||
|
||||
private function canUseCurrentMonthPeriod( $startDate )
|
||||
{
|
||||
if ( !is_string( $startDate ) || !preg_match( '/^\d{4}-\d{2}-\d{2}$/', $startDate ) )
|
||||
return false;
|
||||
|
||||
return $startDate === date( 'Y-m-01' );
|
||||
}
|
||||
}
|
||||
318
autoload/Domain/Finances/FakturowniaImportRepository.php
Normal file
318
autoload/Domain/Finances/FakturowniaImportRepository.php
Normal file
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
namespace Domain\Finances;
|
||||
|
||||
class FakturowniaImportRepository
|
||||
{
|
||||
private $mdb;
|
||||
private $tablesReady = false;
|
||||
|
||||
public function __construct( $mdb = null )
|
||||
{
|
||||
if ( $mdb )
|
||||
$this -> mdb = $mdb;
|
||||
else
|
||||
{
|
||||
global $mdb;
|
||||
$this -> mdb = $mdb;
|
||||
}
|
||||
}
|
||||
|
||||
public function ensureTables()
|
||||
{
|
||||
if ( $this -> tablesReady )
|
||||
return;
|
||||
|
||||
$this -> mdb -> query(
|
||||
'CREATE TABLE IF NOT EXISTS `fakturownia_client_mappings` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`external_client_key` VARCHAR(191) NOT NULL,
|
||||
`external_name` VARCHAR(255) NOT NULL,
|
||||
`crm_client_id` INT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_external_client_key` (`external_client_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
|
||||
$this -> mdb -> query(
|
||||
'CREATE TABLE IF NOT EXISTS `fakturownia_item_mappings` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`external_item_key` VARCHAR(191) NOT NULL,
|
||||
`external_name` VARCHAR(255) NOT NULL,
|
||||
`finance_category_id` INT UNSIGNED NOT NULL,
|
||||
`created_at` DATETIME NOT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_external_item_key` (`external_item_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
|
||||
$this -> mdb -> query(
|
||||
'CREATE TABLE IF NOT EXISTS `fakturownia_imported_documents` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`external_document_key` VARCHAR(191) NOT NULL,
|
||||
`document_type` VARCHAR(32) NOT NULL,
|
||||
`external_id` VARCHAR(64) NOT NULL,
|
||||
`source_date` DATE NULL,
|
||||
`amount` DECIMAL(12,2) NOT NULL DEFAULT 0.00,
|
||||
`finance_operation_ids` TEXT NULL,
|
||||
`meta_json` LONGTEXT NULL,
|
||||
`imported_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_external_document_key` (`external_document_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
|
||||
$this -> mdb -> query(
|
||||
'CREATE TABLE IF NOT EXISTS `fakturownia_unmapped_queue` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`queue_type` VARCHAR(32) NOT NULL,
|
||||
`external_key` VARCHAR(191) NOT NULL,
|
||||
`external_name` VARCHAR(255) NOT NULL,
|
||||
`payload_json` LONGTEXT NULL,
|
||||
`hits` INT UNSIGNED NOT NULL DEFAULT 1,
|
||||
`resolved` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`first_seen_at` DATETIME NOT NULL,
|
||||
`last_seen_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_type_key` (`queue_type`, `external_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
|
||||
$this -> mdb -> query(
|
||||
'CREATE TABLE IF NOT EXISTS `fakturownia_import_state` (
|
||||
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`state_key` VARCHAR(191) NOT NULL,
|
||||
`state_value` LONGTEXT NULL,
|
||||
`updated_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uniq_state_key` (`state_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8'
|
||||
);
|
||||
|
||||
$this -> tablesReady = true;
|
||||
}
|
||||
|
||||
public function getClientMapping( $externalClientKey )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
return $this -> mdb -> get( 'fakturownia_client_mappings', '*', [
|
||||
'external_client_key' => $externalClientKey
|
||||
] );
|
||||
}
|
||||
|
||||
public function saveClientMapping( $externalClientKey, $externalName, $crmClientId )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
|
||||
$current = $this -> getClientMapping( $externalClientKey );
|
||||
$now = date( 'Y-m-d H:i:s' );
|
||||
|
||||
if ( $current )
|
||||
{
|
||||
$this -> mdb -> update( 'fakturownia_client_mappings', [
|
||||
'external_name' => $externalName,
|
||||
'crm_client_id' => (int)$crmClientId,
|
||||
'updated_at' => $now
|
||||
], [ 'id' => (int)$current['id'] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this -> mdb -> insert( 'fakturownia_client_mappings', [
|
||||
'external_client_key' => $externalClientKey,
|
||||
'external_name' => $externalName,
|
||||
'crm_client_id' => (int)$crmClientId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
] );
|
||||
}
|
||||
|
||||
$this -> resolveQueueItem( 'client', $externalClientKey );
|
||||
}
|
||||
|
||||
public function getItemMapping( $externalItemKey )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
return $this -> mdb -> get( 'fakturownia_item_mappings', '*', [
|
||||
'external_item_key' => $externalItemKey
|
||||
] );
|
||||
}
|
||||
|
||||
public function saveItemMapping( $externalItemKey, $externalName, $financeCategoryId )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
|
||||
$current = $this -> getItemMapping( $externalItemKey );
|
||||
$now = date( 'Y-m-d H:i:s' );
|
||||
|
||||
if ( $current )
|
||||
{
|
||||
$this -> mdb -> update( 'fakturownia_item_mappings', [
|
||||
'external_name' => $externalName,
|
||||
'finance_category_id' => (int)$financeCategoryId,
|
||||
'updated_at' => $now
|
||||
], [ 'id' => (int)$current['id'] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this -> mdb -> insert( 'fakturownia_item_mappings', [
|
||||
'external_item_key' => $externalItemKey,
|
||||
'external_name' => $externalName,
|
||||
'finance_category_id' => (int)$financeCategoryId,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now
|
||||
] );
|
||||
}
|
||||
|
||||
$this -> resolveQueueItem( 'item', $externalItemKey );
|
||||
}
|
||||
|
||||
public function isDocumentImported( $externalDocumentKey )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
return (bool)$this -> mdb -> has( 'fakturownia_imported_documents', [
|
||||
'external_document_key' => $externalDocumentKey
|
||||
] );
|
||||
}
|
||||
|
||||
public function markDocumentImported( $externalDocumentKey, $documentType, $externalId, $sourceDate, $amount, $operationIds, $meta )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
|
||||
$now = date( 'Y-m-d H:i:s' );
|
||||
$data = [
|
||||
'external_document_key' => $externalDocumentKey,
|
||||
'document_type' => $documentType,
|
||||
'external_id' => (string)$externalId,
|
||||
'source_date' => $sourceDate,
|
||||
'amount' => (float)$amount,
|
||||
'finance_operation_ids' => implode( ',', $operationIds ),
|
||||
'meta_json' => json_encode( $meta, JSON_UNESCAPED_UNICODE ),
|
||||
'imported_at' => $now
|
||||
];
|
||||
|
||||
$existing = $this -> mdb -> get( 'fakturownia_imported_documents', 'id', [
|
||||
'external_document_key' => $externalDocumentKey
|
||||
] );
|
||||
|
||||
if ( $existing )
|
||||
{
|
||||
$this -> mdb -> update( 'fakturownia_imported_documents', $data, [
|
||||
'id' => (int)$existing
|
||||
] );
|
||||
return;
|
||||
}
|
||||
|
||||
$this -> mdb -> insert( 'fakturownia_imported_documents', $data );
|
||||
}
|
||||
|
||||
public function queueUnmapped( $queueType, $externalKey, $externalName, $payload )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
|
||||
$existing = $this -> mdb -> get( 'fakturownia_unmapped_queue', '*', [
|
||||
'AND' => [
|
||||
'queue_type' => $queueType,
|
||||
'external_key' => $externalKey
|
||||
]
|
||||
] );
|
||||
|
||||
$now = date( 'Y-m-d H:i:s' );
|
||||
$payloadJson = json_encode( $payload, JSON_UNESCAPED_UNICODE );
|
||||
|
||||
if ( $existing )
|
||||
{
|
||||
$this -> mdb -> update( 'fakturownia_unmapped_queue', [
|
||||
'external_name' => $externalName,
|
||||
'payload_json' => $payloadJson,
|
||||
'hits[+]' => 1,
|
||||
'resolved' => 0,
|
||||
'last_seen_at' => $now
|
||||
], [ 'id' => (int)$existing['id'] ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$this -> mdb -> insert( 'fakturownia_unmapped_queue', [
|
||||
'queue_type' => $queueType,
|
||||
'external_key' => $externalKey,
|
||||
'external_name' => $externalName,
|
||||
'payload_json' => $payloadJson,
|
||||
'hits' => 1,
|
||||
'resolved' => 0,
|
||||
'first_seen_at' => $now,
|
||||
'last_seen_at' => $now
|
||||
] );
|
||||
}
|
||||
|
||||
public function pendingClientMappings()
|
||||
{
|
||||
$this -> ensureTables();
|
||||
return $this -> mdb -> select( 'fakturownia_unmapped_queue', '*', [
|
||||
'AND' => [
|
||||
'queue_type' => 'client',
|
||||
'resolved' => 0
|
||||
],
|
||||
'ORDER' => [ 'last_seen_at' => 'DESC' ]
|
||||
] );
|
||||
}
|
||||
|
||||
public function pendingItemMappings()
|
||||
{
|
||||
$this -> ensureTables();
|
||||
return $this -> mdb -> select( 'fakturownia_unmapped_queue', '*', [
|
||||
'AND' => [
|
||||
'queue_type' => 'item',
|
||||
'resolved' => 0,
|
||||
'external_key[!]' => 'name:faktura bez pozycji'
|
||||
],
|
||||
'ORDER' => [ 'last_seen_at' => 'DESC' ]
|
||||
] );
|
||||
}
|
||||
|
||||
public function saveState( $stateKey, $stateValue )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
|
||||
$existing = $this -> mdb -> get( 'fakturownia_import_state', '*', [
|
||||
'state_key' => $stateKey
|
||||
] );
|
||||
$now = date( 'Y-m-d H:i:s' );
|
||||
|
||||
if ( $existing )
|
||||
{
|
||||
$this -> mdb -> update( 'fakturownia_import_state', [
|
||||
'state_value' => $stateValue,
|
||||
'updated_at' => $now
|
||||
], [ 'id' => (int)$existing['id'] ] );
|
||||
return;
|
||||
}
|
||||
|
||||
$this -> mdb -> insert( 'fakturownia_import_state', [
|
||||
'state_key' => $stateKey,
|
||||
'state_value' => $stateValue,
|
||||
'updated_at' => $now
|
||||
] );
|
||||
}
|
||||
|
||||
public function getState( $stateKey )
|
||||
{
|
||||
$this -> ensureTables();
|
||||
return $this -> mdb -> get( 'fakturownia_import_state', 'state_value', [
|
||||
'state_key' => $stateKey
|
||||
] );
|
||||
}
|
||||
|
||||
private function resolveQueueItem( $queueType, $externalKey )
|
||||
{
|
||||
$this -> mdb -> update( 'fakturownia_unmapped_queue', [
|
||||
'resolved' => 1,
|
||||
'last_seen_at' => date( 'Y-m-d H:i:s' )
|
||||
], [
|
||||
'AND' => [
|
||||
'queue_type' => $queueType,
|
||||
'external_key' => $externalKey
|
||||
]
|
||||
] );
|
||||
}
|
||||
}
|
||||
461
autoload/Domain/Finances/FakturowniaInvoiceImporter.php
Normal file
461
autoload/Domain/Finances/FakturowniaInvoiceImporter.php
Normal file
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
namespace Domain\Finances;
|
||||
|
||||
class FakturowniaInvoiceImporter
|
||||
{
|
||||
private $mdb;
|
||||
private $repo;
|
||||
private $apiClient;
|
||||
private $startDate;
|
||||
private $pageLimit = 100;
|
||||
|
||||
public function __construct( $mdb = null )
|
||||
{
|
||||
if ( $mdb )
|
||||
$this -> mdb = $mdb;
|
||||
else
|
||||
{
|
||||
global $mdb;
|
||||
$this -> mdb = $mdb;
|
||||
}
|
||||
|
||||
$this -> repo = new FakturowniaImportRepository( $this -> mdb );
|
||||
}
|
||||
|
||||
public function import()
|
||||
{
|
||||
$this -> repo -> ensureTables();
|
||||
|
||||
$config = $this -> resolveConfig();
|
||||
if ( !$config['ok'] )
|
||||
return $config;
|
||||
|
||||
$this -> startDate = $config['start_date'];
|
||||
$this -> apiClient = new FakturowniaApiClient(
|
||||
$config['api_url'],
|
||||
$config['token'],
|
||||
$config['page_limit']
|
||||
);
|
||||
$this -> pageLimit = (int)$config['page_limit'];
|
||||
|
||||
$summary = [
|
||||
'imported' => 0,
|
||||
'skipped' => 0,
|
||||
'unmapped' => 0,
|
||||
'errors' => 0
|
||||
];
|
||||
|
||||
$summary = $this -> processDocumentType( 'income', $summary );
|
||||
$summary = $this -> processDocumentType( 'cost', $summary );
|
||||
|
||||
$lastSummary = json_encode( [
|
||||
'at' => date( 'Y-m-d H:i:s' ),
|
||||
'summary' => $summary
|
||||
], JSON_UNESCAPED_UNICODE );
|
||||
$this -> repo -> saveState( 'last_import_summary', $lastSummary );
|
||||
|
||||
if ( $summary['imported'] === 0 && $summary['unmapped'] === 0 && $summary['errors'] === 0 )
|
||||
return [ 'status' => 'empty', 'msg' => 'Import Fakturownia: brak nowych dokumentow.' ];
|
||||
|
||||
if ( $summary['errors'] > 0 )
|
||||
return [ 'status' => 'error', 'msg' => $this -> formatMessage( $summary ), 'summary' => $summary ];
|
||||
|
||||
return [ 'status' => 'ok', 'msg' => $this -> formatMessage( $summary ), 'summary' => $summary ];
|
||||
}
|
||||
|
||||
private function processDocumentType( $documentType, $summary )
|
||||
{
|
||||
$page = 1;
|
||||
|
||||
while ( true )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $documentType === 'income' )
|
||||
$documents = $this -> apiClient -> fetchSalesDocuments( $this -> startDate, $page );
|
||||
else
|
||||
$documents = $this -> apiClient -> fetchCostDocuments( $this -> startDate, $page );
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
$summary['errors']++;
|
||||
break;
|
||||
}
|
||||
|
||||
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 ]++;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
$page++;
|
||||
if ( $page > 100 )
|
||||
break;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
private function processSingleDocument( $rawDocument, $documentType )
|
||||
{
|
||||
if ( !$this -> matchesDocumentType( $rawDocument, $documentType ) )
|
||||
return 'skipped';
|
||||
|
||||
$document = $this -> normalizeDocument( $rawDocument, $documentType );
|
||||
if ( !$document )
|
||||
return 'skipped';
|
||||
|
||||
if ( $this -> repo -> isDocumentImported( $document['document_key'] ) )
|
||||
return 'skipped';
|
||||
|
||||
if ( strtotime( $document['date'] ) < strtotime( $this -> startDate ) )
|
||||
return 'skipped';
|
||||
|
||||
$clientMap = $this -> repo -> getClientMapping( $document['client_key'] );
|
||||
if ( !$clientMap )
|
||||
{
|
||||
$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']
|
||||
] );
|
||||
return 'unmapped';
|
||||
}
|
||||
|
||||
$resolvedPositions = [];
|
||||
foreach ( $document['positions'] as $position )
|
||||
{
|
||||
$itemMap = $this -> repo -> getItemMapping( $position['item_key'] );
|
||||
if ( !$itemMap )
|
||||
{
|
||||
$this -> repo -> queueUnmapped( 'item', $position['item_key'], $position['name'], [
|
||||
'document_id' => $document['external_id'],
|
||||
'document_number' => $document['number'],
|
||||
'document_type' => $documentType
|
||||
] );
|
||||
return 'unmapped';
|
||||
}
|
||||
|
||||
$amount = $this -> normalizeAmount( $position['amount'], $documentType );
|
||||
if ( $amount == 0.0 )
|
||||
continue;
|
||||
|
||||
$resolvedPositions[] = [
|
||||
'category_id' => (int)$itemMap['finance_category_id'],
|
||||
'amount' => $amount,
|
||||
'description' => $this -> buildDescription( $document, $position )
|
||||
];
|
||||
}
|
||||
|
||||
if ( empty( $resolvedPositions ) )
|
||||
return 'skipped';
|
||||
|
||||
$operationIds = [];
|
||||
|
||||
// Cala faktura importuje sie atomowo: albo wszystkie pozycje, albo nic.
|
||||
$this -> mdb -> pdo -> beginTransaction();
|
||||
try
|
||||
{
|
||||
foreach ( $resolvedPositions as $resolvedPosition )
|
||||
{
|
||||
$this -> mdb -> insert( 'finance_operations', [
|
||||
'date' => $document['date'],
|
||||
'category_id' => $resolvedPosition['category_id'],
|
||||
'amount' => $resolvedPosition['amount'],
|
||||
'description' => $resolvedPosition['description'],
|
||||
'client_id' => (int)$clientMap['crm_client_id']
|
||||
] );
|
||||
|
||||
$operationIds[] = (int)$this -> mdb -> id();
|
||||
}
|
||||
|
||||
$this -> repo -> markDocumentImported(
|
||||
$document['document_key'],
|
||||
$documentType,
|
||||
$document['external_id'],
|
||||
$document['date'],
|
||||
$document['total_amount'],
|
||||
$operationIds,
|
||||
[
|
||||
'number' => $document['number'],
|
||||
'client_name' => $document['client_name']
|
||||
]
|
||||
);
|
||||
|
||||
$this -> mdb -> pdo -> commit();
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
if ( $this -> mdb -> pdo ->inTransaction() )
|
||||
$this -> mdb -> pdo -> rollBack();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return 'imported';
|
||||
}
|
||||
|
||||
private function matchesDocumentType( $rawDocument, $documentType )
|
||||
{
|
||||
if ( isset( $rawDocument['invoice'] ) && is_array( $rawDocument['invoice'] ) )
|
||||
$rawDocument = $rawDocument['invoice'];
|
||||
|
||||
if ( !is_array( $rawDocument ) )
|
||||
return false;
|
||||
|
||||
if ( !array_key_exists( 'income', $rawDocument ) )
|
||||
return true;
|
||||
|
||||
$incomeFlag = $rawDocument['income'];
|
||||
$isIncome = !in_array( $incomeFlag, [ false, 0, '0', 'false', 'FALSE' ], true );
|
||||
|
||||
if ( $documentType === 'income' )
|
||||
return $isIncome;
|
||||
|
||||
return !$isIncome;
|
||||
}
|
||||
|
||||
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'] ) )
|
||||
$rawDocument = $rawDocument['invoice'];
|
||||
|
||||
if ( !is_array( $rawDocument ) )
|
||||
return null;
|
||||
|
||||
$externalId = isset( $rawDocument['id'] ) ? (string)$rawDocument['id'] : '';
|
||||
if ( $externalId === '' )
|
||||
return null;
|
||||
|
||||
$number = (string)( $rawDocument['number'] ?? $rawDocument['full_number'] ?? '' );
|
||||
$date = (string)( $rawDocument['issue_date'] ?? $rawDocument['sell_date'] ?? $rawDocument['created_at'] ?? date( 'Y-m-d' ) );
|
||||
$date = substr( $date, 0, 10 );
|
||||
|
||||
if ( $documentType === 'income' )
|
||||
{
|
||||
$clientName = (string)( $rawDocument['buyer_name'] ?? $rawDocument['client_name'] ?? 'Nieznany klient' );
|
||||
$clientTaxNo = (string)( $rawDocument['buyer_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$clientName = (string)( $rawDocument['seller_name'] ?? $rawDocument['client_name'] ?? 'Nieznany kontrahent' );
|
||||
$clientTaxNo = (string)( $rawDocument['seller_tax_no'] ?? $rawDocument['tax_no'] ?? '' );
|
||||
}
|
||||
|
||||
$positions = [];
|
||||
$totalAmount = 0.0;
|
||||
|
||||
$rawPositions = $this -> resolvePositions( $rawDocument );
|
||||
foreach ( $rawPositions as $rawPosition )
|
||||
{
|
||||
if ( !is_array( $rawPosition ) )
|
||||
continue;
|
||||
|
||||
$name = trim( (string)( $rawPosition['name'] ?? '' ) );
|
||||
if ( $name === '' )
|
||||
$name = 'Pozycja';
|
||||
|
||||
$amount = $this -> resolvePositionAmount( $rawPosition );
|
||||
$totalAmount += $amount;
|
||||
|
||||
$positions[] = [
|
||||
'item_key' => $this -> buildItemKey( $rawPosition, $name ),
|
||||
'name' => $name,
|
||||
'amount' => $amount
|
||||
];
|
||||
}
|
||||
|
||||
if ( empty( $positions ) )
|
||||
{
|
||||
$fallback = (float)( $rawDocument['price_gross'] ?? $rawDocument['price'] ?? 0 );
|
||||
$fallbackName = trim( (string)( $rawDocument['product_cache'] ?? '' ) );
|
||||
if ( $fallbackName === '' )
|
||||
$fallbackName = 'Faktura bez pozycji';
|
||||
|
||||
if ( $fallback != 0.0 )
|
||||
{
|
||||
$positions[] = [
|
||||
'item_key' => $this -> buildItemKey( [], $fallbackName ),
|
||||
'name' => $fallbackName,
|
||||
'amount' => $fallback
|
||||
];
|
||||
$totalAmount = $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if ( empty( $positions ) )
|
||||
return null;
|
||||
|
||||
return [
|
||||
'external_id' => $externalId,
|
||||
'document_key' => $documentType . ':' . $externalId,
|
||||
'number' => $number,
|
||||
'date' => $date,
|
||||
'client_name' => $clientName,
|
||||
'client_tax_no' => $clientTaxNo,
|
||||
'client_key' => $this -> buildClientKey( $rawDocument, $clientName, $clientTaxNo ),
|
||||
'positions' => $positions,
|
||||
'total_amount' => $this -> normalizeAmount( $totalAmount, $documentType )
|
||||
];
|
||||
}
|
||||
|
||||
private function resolvePositions( $rawDocument )
|
||||
{
|
||||
if ( isset( $rawDocument['positions'] ) && is_array( $rawDocument['positions'] ) && !empty( $rawDocument['positions'] ) )
|
||||
return $rawDocument['positions'];
|
||||
|
||||
$invoiceId = isset( $rawDocument['id'] ) ? (int)$rawDocument['id'] : 0;
|
||||
if ( $invoiceId <= 0 )
|
||||
return [];
|
||||
|
||||
$details = $this -> apiClient -> fetchInvoiceDetails( $invoiceId );
|
||||
if ( is_array( $details ) && isset( $details['positions'] ) && is_array( $details['positions'] ) )
|
||||
return $details['positions'];
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private function resolvePositionAmount( $position )
|
||||
{
|
||||
$quantity = (float)( $position['quantity'] ?? 1 );
|
||||
if ( $quantity <= 0 )
|
||||
$quantity = 1;
|
||||
|
||||
$totalNet = (float)( $position['total_price_net'] ?? 0 );
|
||||
if ( $totalNet != 0.0 )
|
||||
return $totalNet;
|
||||
|
||||
$unitNet = (float)( $position['price'] ?? $position['price_net'] ?? 0 );
|
||||
if ( $unitNet != 0.0 )
|
||||
return $unitNet * $quantity;
|
||||
|
||||
// Fallback dla pozycji, ktore nie maja netto w payloadzie.
|
||||
$totalGross = (float)( $position['total_price_gross'] ?? 0 );
|
||||
if ( $totalGross != 0.0 )
|
||||
return $totalGross;
|
||||
|
||||
$unitGross = (float)( $position['price_gross'] ?? 0 );
|
||||
if ( $unitGross != 0.0 )
|
||||
return $unitGross * $quantity;
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private function buildClientKey( $rawDocument, $clientName, $clientTaxNo )
|
||||
{
|
||||
if ( isset( $rawDocument['client_id'] ) && (string)$rawDocument['client_id'] !== '' )
|
||||
return 'id:' . (string)$rawDocument['client_id'];
|
||||
|
||||
if ( $clientTaxNo !== '' )
|
||||
return 'tax:' . preg_replace( '/\s+/', '', $clientTaxNo );
|
||||
|
||||
return 'name:' . $this -> normalizeKeyValue( $clientName );
|
||||
}
|
||||
|
||||
private function buildItemKey( $rawPosition, $name )
|
||||
{
|
||||
if ( isset( $rawPosition['product_id'] ) && (string)$rawPosition['product_id'] !== '' )
|
||||
return 'product:' . (string)$rawPosition['product_id'];
|
||||
|
||||
return 'name:' . $this -> normalizeKeyValue( $name );
|
||||
}
|
||||
|
||||
private function normalizeKeyValue( $value )
|
||||
{
|
||||
$value = trim( (string)$value );
|
||||
if ( function_exists( 'mb_strtolower' ) )
|
||||
return mb_strtolower( $value, 'UTF-8' );
|
||||
|
||||
return strtolower( $value );
|
||||
}
|
||||
|
||||
private function normalizeAmount( $amount, $documentType )
|
||||
{
|
||||
$amount = (float)$amount;
|
||||
if ( $documentType === 'cost' )
|
||||
return abs( $amount ) * -1;
|
||||
|
||||
return abs( $amount );
|
||||
}
|
||||
|
||||
private function buildDescription( $document, $position )
|
||||
{
|
||||
$parts = [
|
||||
'Fakturownia'
|
||||
];
|
||||
|
||||
if ( $document['number'] !== '' )
|
||||
$parts[] = 'FV: ' . $document['number'];
|
||||
|
||||
$parts[] = $position['name'];
|
||||
$parts[] = $document['client_name'];
|
||||
|
||||
return implode( ' | ', $parts );
|
||||
}
|
||||
|
||||
private function resolveConfig()
|
||||
{
|
||||
$domain = trim( (string)\Env::get( 'FAKTUROWNIA_API_DOMAIN', '' ) );
|
||||
$token = trim( (string)\Env::get( 'FAKTUROWNIA_API_TOKEN', '' ) );
|
||||
$startDate = trim( (string)\Env::get( 'FAKTUROWNIA_START_DATE', '' ) );
|
||||
$pageLimit = (int)\Env::get( 'FAKTUROWNIA_PAGE_LIMIT', 100 );
|
||||
|
||||
if ( $domain === '' || $token === '' || $startDate === '' )
|
||||
return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: brak konfiguracji w .env.' ];
|
||||
|
||||
if ( !preg_match( '/^\d{4}-\d{2}-\d{2}$/', $startDate ) )
|
||||
return [ 'status' => 'error', 'ok' => false, 'msg' => 'Import Fakturownia: nieprawidlowa data FAKTUROWNIA_START_DATE.' ];
|
||||
|
||||
if ( strpos( $domain, 'http://' ) !== 0 && strpos( $domain, 'https://' ) !== 0 )
|
||||
$domain = 'https://' . $domain;
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'api_url' => rtrim( $domain, '/' ),
|
||||
'token' => $token,
|
||||
'start_date' => $startDate,
|
||||
'page_limit' => $pageLimit > 0 ? $pageLimit : 100
|
||||
];
|
||||
}
|
||||
|
||||
private function formatMessage( $summary )
|
||||
{
|
||||
return 'Import Fakturownia: zaimportowano ' . (int)$summary['imported']
|
||||
. ', pominieto ' . (int)$summary['skipped']
|
||||
. ', brak mapowan ' . (int)$summary['unmapped']
|
||||
. ', bledy ' . (int)$summary['errors'] . '.';
|
||||
}
|
||||
}
|
||||
@@ -55,6 +55,23 @@ class FinanceRepository
|
||||
return $this -> mdb -> select( 'crm_client', [ 'id', 'firm' ], [ 'ORDER' => [ 'firm' => 'ASC' ] ] );
|
||||
}
|
||||
|
||||
public function categoriesFlatList()
|
||||
{
|
||||
return $this -> mdb -> select( 'finance_categories', [ 'id', 'name', 'group_id', 'parent_id' ], [
|
||||
'ORDER' => [ 'name' => 'ASC' ]
|
||||
] );
|
||||
}
|
||||
|
||||
public function clientExists( $clientId )
|
||||
{
|
||||
return (bool)$this -> mdb -> has( 'crm_client', [ 'id' => (int)$clientId ] );
|
||||
}
|
||||
|
||||
public function categoryExists( $categoryId )
|
||||
{
|
||||
return (bool)$this -> mdb -> has( 'finance_categories', [ 'id' => (int)$categoryId ] );
|
||||
}
|
||||
|
||||
public function clientsWithRevenue( $date_from, $date_to, $group_id )
|
||||
{
|
||||
return $this -> mdb -> query(
|
||||
|
||||
Reference in New Issue
Block a user