Files
crmPRO/autoload/Domain/Finances/FinanceRepository.php
Codex 7acf22c71a feat(05-finances-fakturownia-import): complete Fakturownia mapping corrections
Phase 5 complete:
- add category mapping edit from operation edit for Fakturownia operations
- update current operation category immediately after mapping change
- support optional bulk update for matching imported operations
- close 05-05 and 05-06 PAUL summaries

Co-Authored-By: Codex <noreply@openai.com>
2026-05-04 22:57:55 +02:00

578 lines
20 KiB
PHP

<?php
namespace Domain\Finances;
class FinanceRepository
{
private $mdb;
public function __construct( $mdb = null )
{
if ( $mdb )
$this -> mdb = $mdb;
else
{
global $mdb;
$this -> mdb = $mdb;
}
}
public function firstOperationDate()
{
return $this -> mdb -> get( 'finance_operations', 'date', [ 'ORDER' => [ 'date' => 'ASC' ] ] );
}
public function getOperationTags( int $operation_id )
{
$return = '';
$tags_id = $this -> mdb -> select( 'finance_operation_tags', 'tag_id', [ 'operation_id' => $operation_id ] );
foreach ( $tags_id as $tag_id )
{
if ( $return )
$return .= ', ';
$return .= $this -> mdb -> get( 'finance_tags', 'tag', [ 'id' => $tag_id ] );
}
return $return;
}
public function clientName( $client_id )
{
return $this -> mdb -> get( 'crm_client', 'firm', [ 'id' => $client_id ] );
}
public function clientsListByDates( $date_from, $date_to )
{
return $this -> mdb -> query(
'SELECT cc.id, cc.firm FROM crm_client AS cc '
. 'INNER JOIN finance_operations AS fo ON fo.client_id = cc.id '
. 'WHERE date >= :date_from AND date <= :date_to '
. 'GROUP BY cc.id ORDER BY firm ASC',
[ ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll( \PDO::FETCH_ASSOC );
}
public function clientsList()
{
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(
'SELECT cc.id, cc.firm, '
. 'SUM( CASE WHEN fo.amount > 0 THEN fo.amount ELSE 0 END ) AS income, '
. 'SUM( CASE WHEN fo.amount < 0 THEN fo.amount ELSE 0 END ) AS costs, '
. 'SUM( fo.amount ) AS total, '
. 'COUNT( fo.id ) AS operations_count '
. 'FROM crm_client AS cc '
. 'INNER JOIN finance_operations AS fo ON fo.client_id = cc.id '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'WHERE fo.date >= :date_from AND fo.date <= :date_to AND fc.group_id = :group_id '
. 'GROUP BY cc.id '
. 'ORDER BY total DESC',
[ ':date_from' => $date_from, ':date_to' => $date_to, ':group_id' => (int)$group_id ]
) -> fetchAll( \PDO::FETCH_ASSOC );
}
public function deleteCategory( $category_id )
{
return $this -> mdb -> delete( 'finance_categories', [ 'id' => (int)$category_id ] );
}
public function defaultGroup()
{
return $this -> mdb -> get( 'finance_group', 'id', [ 'default_group' => 1 ] );
}
public function groupsList()
{
return $this -> mdb -> select( 'finance_group', '*', [ 'ORDER' => [ 'name' => 'ASC' ] ] );
}
public function deleteOperation( $operation_id )
{
return $this -> mdb -> delete( 'finance_operations', [ 'id' => $operation_id ] );
}
public function tagsJson( $group_id )
{
return $this -> mdb -> select( 'finance_tags', 'tag', [ 'group_id' => $group_id, 'ORDER' => [ 'tag' => 'ASC' ] ] );
}
public function tagsList( $group_id )
{
$tags = [];
$results = $this -> mdb -> select( 'finance_tags', '*', [ 'group_id' => $group_id ] );
if ( is_array( $results ) and !empty( $results ) ) foreach ( $results as $row )
{
$tag = $row;
$tag['count'] = $this -> mdb -> count( 'finance_operation_tags', [ 'tag_id' => $row['id'] ] );
$tags[] = $tag;
}
if ( count( $tags ) )
array_multisort( array_column( $tags, 'count' ), SORT_DESC, $tags );
return $tags;
}
public function operationsList( $date_from, $date_to, $group_id, $client_id = null )
{
$sql = 'SELECT fo.*, fc.name '
. 'FROM finance_operations AS fo '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'WHERE date >= :date_from AND date <= :date_to AND group_id = :group_id ';
$params = [ ':date_from' => $date_from, ':date_to' => $date_to, ':group_id' => (int)$group_id ];
if ( $client_id )
{
$sql .= 'AND fo.client_id = :client_id ';
$params[':client_id'] = (int)$client_id;
}
$sql .= 'ORDER BY fo.date DESC, fo.id DESC';
return $this -> mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
}
public function operationDetails( $operation_id )
{
$operation = $this -> mdb -> get( 'finance_operations', '*', [ 'id' => $operation_id ] );
if ( !$operation )
return [];
$operation['tags'] = $this -> mdb -> query(
'SELECT tag, tag_id FROM finance_operation_tags AS fot '
. 'INNER JOIN finance_tags AS ft ON fot.tag_id = ft.id '
. 'WHERE operation_id = ' . (int)$operation_id
) -> fetchAll( \PDO::FETCH_ASSOC );
return $operation;
}
public function fakturowniaOperationContext( $operationId )
{
$operationId = (int)$operationId;
if ( $operationId <= 0 )
return null;
$operation = $this -> mdb -> get( 'finance_operations', '*', [ 'id' => $operationId ] );
if ( !$operation )
return null;
$imported = $this -> mdb -> query(
'SELECT id FROM fakturownia_imported_documents '
. 'WHERE FIND_IN_SET( :operation_id, finance_operation_ids ) LIMIT 1',
[ ':operation_id' => (string)$operationId ]
) -> fetch( \PDO::FETCH_ASSOC );
if ( !$imported )
return null;
$itemName = $this -> extractItemNameFromDescription( (string)( $operation['description'] ?? '' ) );
if ( $itemName === '' )
return null;
$externalItemKey = $this -> buildNameItemKey( $itemName );
$mapping = $this -> mdb -> get( 'fakturownia_item_mappings', '*', [
'external_item_key' => $externalItemKey
] );
if ( !$mapping )
$mapping = $this -> findItemMappingByExternalName( $itemName );
if ( !$mapping )
return null;
return [
'operation_id' => $operationId,
'item_name' => $itemName,
'external_item_key' => (string)$mapping['external_item_key'],
'external_name' => (string)$mapping['external_name'],
'finance_category_id' => (int)$mapping['finance_category_id']
];
}
public function updateOperationCategory( $operationId, $categoryId )
{
$this -> mdb -> update( 'finance_operations', [
'category_id' => (int)$categoryId
], [
'id' => (int)$operationId
] );
}
public function updateFakturowniaOperationsCategoryByItemName( $itemName, $categoryId, $includeAll )
{
$itemName = trim( (string)$itemName );
if ( $itemName === '' )
return 0;
if ( !$includeAll )
return 0;
$pattern = '%| ' . $itemName . ' |%';
$stmt = $this -> mdb -> query(
'UPDATE finance_operations AS fo '
. 'INNER JOIN fakturownia_imported_documents AS fid ON FIND_IN_SET( fo.id, fid.finance_operation_ids ) '
. 'INNER JOIN finance_categories AS fc_old ON fc_old.id = fo.category_id '
. 'INNER JOIN finance_categories AS fc_new ON fc_new.id = :category_id '
. 'SET fo.category_id = :category_id '
. 'WHERE fo.description LIKE :pattern AND fc_old.group_id = fc_new.group_id',
[ ':category_id' => (int)$categoryId, ':pattern' => $pattern ]
);
if ( method_exists( $stmt, 'rowCount' ) )
return (int)$stmt -> rowCount();
return 0;
}
public function operationGroupId( $operationId )
{
$row = $this -> mdb -> query(
'SELECT fc.group_id '
. 'FROM finance_operations AS fo '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'WHERE fo.id = :operation_id LIMIT 1',
[ ':operation_id' => (int)$operationId ]
) -> fetch( \PDO::FETCH_ASSOC );
return isset( $row['group_id'] ) ? (int)$row['group_id'] : 0;
}
public function categoryGroupId( $categoryId )
{
return (int)$this -> mdb -> get( 'finance_categories', 'group_id', [ 'id' => (int)$categoryId ] );
}
public function saveOperation( $operation_id, $category_id, $date, $amount, $description, $tags, $group_id, $client_id = null )
{
$data = [
'date' => $date ? $date : date( 'Y-m-d' ),
'category_id' => $category_id,
'amount' => $amount,
'description' => $description,
'client_id' => $client_id ? (int)$client_id : null
];
if ( !$operation_id )
{
$this -> mdb -> insert( 'finance_operations', $data );
$id = $this -> mdb -> id();
}
else
{
$this -> mdb -> update( 'finance_operations', $data, [ 'id' => $operation_id ] );
$this -> mdb -> delete( 'finance_operation_tags', [ 'operation_id' => $operation_id ] );
$id = $operation_id;
}
$this -> syncOperationTags( $id, $tags, $group_id );
return $id;
}
private function syncOperationTags( $operation_id, $tags_string, $group_id )
{
$tags = explode( ',', $tags_string );
if ( !is_array( $tags ) )
return;
foreach ( $tags as $tag )
{
$tag = trim( $tag );
if ( !$tag )
continue;
$tag_id = $this -> mdb -> get( 'finance_tags', 'id', [ 'AND' => [ 'group_id' => $group_id, 'tag' => $tag ] ] );
if ( !$tag_id )
{
$this -> mdb -> insert( 'finance_tags', [ 'tag' => $tag, 'group_id' => $group_id ] );
$tag_id = $this -> mdb -> id();
}
$this -> mdb -> insert( 'finance_operation_tags', [ 'operation_id' => $operation_id, 'tag_id' => $tag_id ] );
}
}
public function categoryDetails( $category_id )
{
return $this -> mdb -> get( 'finance_categories', '*', [ 'id' => $category_id ] );
}
public function saveCategory( $category_id, $name, $parent_id, $group_id )
{
if ( !$category_id )
{
$this -> mdb -> insert( 'finance_categories', [
'name' => $name,
'parent_id' => $parent_id ? $parent_id : null,
'group_id' => $group_id
] );
return $this -> mdb -> id();
}
else
{
$this -> mdb -> update( 'finance_categories', [
'name' => $name,
'group_id' => $group_id
], [ 'id' => $category_id ] );
return $category_id;
}
}
public function walletSummary( $group_id )
{
$results = $this -> mdb -> query(
'SELECT SUM(amount) AS val FROM finance_operations '
. 'WHERE category_id IN ( SELECT id FROM finance_categories WHERE group_id = :group_id )',
[ ':group_id' => (int)$group_id ]
) -> fetchAll( \PDO::FETCH_ASSOC );
return $results[0]['val'];
}
public function walletSummaryThisMonth( $group_id )
{
$date_from = date( 'Y-m-01' );
$date_to = date( 'Y-m-t' );
$results = $this -> mdb -> query(
'SELECT SUM(amount) AS val FROM finance_operations '
. 'WHERE category_id IN ( SELECT id FROM finance_categories WHERE group_id = :group_id ) '
. 'AND date >= :date_from AND date <= :date_to',
[ ':group_id' => (int)$group_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll( \PDO::FETCH_ASSOC );
return $results[0]['val'];
}
public function walletIncomeThisMonth( $group_id )
{
$date_from = date( 'Y-m-01' );
$date_to = date( 'Y-m-t' );
$results = $this -> mdb -> query(
'SELECT SUM(amount) AS val FROM finance_operations '
. 'WHERE category_id IN ( SELECT id FROM finance_categories WHERE group_id = :group_id ) '
. 'AND date >= :date_from AND date <= :date_to AND amount > 0',
[ ':group_id' => (int)$group_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll( \PDO::FETCH_ASSOC );
return $results[0]['val'];
}
public function walletExpensesThisMonth( $group_id )
{
$date_from = date( 'Y-m-01' );
$date_to = date( 'Y-m-t' );
$results = $this -> mdb -> query(
'SELECT SUM(amount) AS val FROM finance_operations '
. 'WHERE category_id IN ( SELECT id FROM finance_categories WHERE group_id = :group_id ) '
. 'AND date >= :date_from AND date <= :date_to AND amount < 0',
[ ':group_id' => (int)$group_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll( \PDO::FETCH_ASSOC );
return $results[0]['val'];
}
public function operations( $category_id, $date_from, $date_to, $tag_id = null )
{
$operations = [];
if ( $tag_id )
$results = $this -> mdb -> query(
'SELECT fo.* FROM finance_operations AS fo '
. 'INNER JOIN finance_operation_tags AS fot ON fot.operation_id = fo.id '
. 'WHERE category_id = :category_id AND date >= :date_from AND date <= :date_to AND tag_id = :tag_id '
. 'ORDER BY date DESC, id DESC',
[ ':category_id' => (int)$category_id, ':date_from' => $date_from, ':date_to' => $date_to, ':tag_id' => (int)$tag_id ]
) -> fetchAll( \PDO::FETCH_ASSOC );
else
$results = $this -> mdb -> query(
'SELECT fo.* FROM finance_operations AS fo '
. 'WHERE category_id = :category_id AND date >= :date_from AND date <= :date_to '
. 'ORDER BY date DESC, fo.id DESC',
[ ':category_id' => (int)$category_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll( \PDO::FETCH_ASSOC );
if ( is_array( $results ) ) foreach ( $results as $row )
{
$row['tags'] = $this -> mdb -> query(
'SELECT tag FROM finance_operation_tags AS fot '
. 'INNER JOIN finance_tags AS ft ON fot.tag_id = ft.id '
. 'WHERE operation_id = ' . (int)$row['id']
) -> fetchAll( \PDO::FETCH_ASSOC );
$operations[] = $row;
}
return $operations;
}
public function categories( $date_from, $date_to, $tag_id = '', $parent_id = null, $group_id, $client_id = null )
{
$categories = [];
$results = $this -> mdb -> select( 'finance_categories', [ 'id', 'name' ], [
'AND' => [ 'group_id' => $group_id, 'parent_id' => $parent_id ],
'ORDER' => [ 'name' => 'ASC' ]
] );
if ( !is_array( $results ) or empty( $results ) )
return $categories;
foreach ( $results as $row )
{
if ( $tag_id )
{
$row['costs'] = (double)$this -> categorySumByTag( $row['id'], $tag_id, $group_id, $date_from, $date_to, '< 0' );
$row['costs_count'] = (int)$this -> categoryCountByTag( $row['id'], $tag_id, $group_id, $date_from, $date_to, '< 0' );
$row['income'] = (double)$this -> categorySumByTag( $row['id'], $tag_id, $group_id, $date_from, $date_to, '> 0' );
$row['income_count'] = (int)$this -> categoryCountByTag( $row['id'], $tag_id, $group_id, $date_from, $date_to, '> 0' );
}
elseif ( $client_id )
{
$row['costs'] = (double)$this -> categorySumByClient( $row['id'], $client_id, $date_from, $date_to, '< 0' );
$row['costs_count'] = (int)$this -> categoryCountByClient( $row['id'], $client_id, $date_from, $date_to, '< 0' );
$row['income'] = (double)$this -> categorySumByClient( $row['id'], $client_id, $date_from, $date_to, '> 0' );
$row['income_count'] = (int)$this -> categoryCountByClient( $row['id'], $client_id, $date_from, $date_to, '> 0' );
}
else
{
$row['costs'] = (double)$this -> categorySum( $row['id'], $date_from, $date_to, '< 0' );
$row['costs_count'] = (int)$this -> categoryCount( $row['id'], $date_from, $date_to, '< 0' );
$row['income'] = (double)$this -> categorySum( $row['id'], $date_from, $date_to, '> 1' );
$row['income_count'] = (int)$this -> categoryCount( $row['id'], $date_from, $date_to, '> 1' );
}
$row['subcategories'] = $this -> categories( $date_from, $date_to, $tag_id, $row['id'], $group_id, $client_id );
$categories[] = $row;
}
return $categories;
}
private function categorySum( $category_id, $date_from, $date_to, $amount_condition )
{
$results = $this -> mdb -> query(
'SELECT SUM(amount) FROM finance_operations '
. 'WHERE category_id = :category_id AND date >= :date_from AND date <= :date_to AND amount ' . $amount_condition,
[ ':category_id' => (int)$category_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll();
return $results[0][0];
}
private function categoryCount( $category_id, $date_from, $date_to, $amount_condition )
{
$results = $this -> mdb -> query(
'SELECT COUNT(0) FROM finance_operations '
. 'WHERE category_id = :category_id AND date >= :date_from AND date <= :date_to AND amount ' . $amount_condition,
[ ':category_id' => (int)$category_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll();
return $results[0][0];
}
private function categorySumByTag( $category_id, $tag_id, $group_id, $date_from, $date_to, $amount_condition )
{
$results = $this -> mdb -> query(
'SELECT SUM(amount) FROM finance_operations AS fo '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'INNER JOIN finance_operation_tags AS fot ON fot.operation_id = fo.id '
. 'INNER JOIN finance_tags AS ft ON ft.id = fot.tag_id '
. 'WHERE category_id = :category_id AND tag_id = :tag_id AND ft.group_id = :group_id '
. 'AND date >= :date_from AND date <= :date_to AND amount ' . $amount_condition,
[ ':category_id' => (int)$category_id, ':tag_id' => (int)$tag_id, ':group_id' => (int)$group_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll();
return $results[0][0];
}
private function categoryCountByTag( $category_id, $tag_id, $group_id, $date_from, $date_to, $amount_condition )
{
$results = $this -> mdb -> query(
'SELECT COUNT(fo.id) FROM finance_operations AS fo '
. 'INNER JOIN finance_categories AS fc ON fc.id = fo.category_id '
. 'INNER JOIN finance_operation_tags AS fot ON fot.operation_id = fo.id '
. 'INNER JOIN finance_tags AS ft ON ft.id = fot.tag_id '
. 'WHERE category_id = :category_id AND tag_id = :tag_id AND ft.group_id = :group_id '
. 'AND date >= :date_from AND date <= :date_to AND amount ' . $amount_condition,
[ ':category_id' => (int)$category_id, ':tag_id' => (int)$tag_id, ':group_id' => (int)$group_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll();
return $results[0][0];
}
private function categorySumByClient( $category_id, $client_id, $date_from, $date_to, $amount_condition )
{
$results = $this -> mdb -> query(
'SELECT SUM(amount) FROM finance_operations '
. 'WHERE category_id = :category_id AND client_id = :client_id '
. 'AND date >= :date_from AND date <= :date_to AND amount ' . $amount_condition,
[ ':category_id' => (int)$category_id, ':client_id' => (int)$client_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll();
return $results[0][0];
}
private function categoryCountByClient( $category_id, $client_id, $date_from, $date_to, $amount_condition )
{
$results = $this -> mdb -> query(
'SELECT COUNT(0) FROM finance_operations '
. 'WHERE category_id = :category_id AND client_id = :client_id '
. 'AND date >= :date_from AND date <= :date_to AND amount ' . $amount_condition,
[ ':category_id' => (int)$category_id, ':client_id' => (int)$client_id, ':date_from' => $date_from, ':date_to' => $date_to ]
) -> fetchAll();
return $results[0][0];
}
private function extractItemNameFromDescription( $description )
{
$description = trim( (string)$description );
if ( $description === '' || strpos( $description, 'Fakturownia |' ) !== 0 )
return '';
$parts = explode( '|', $description );
if ( count( $parts ) < 3 )
return '';
return trim( (string)$parts[2] );
}
private function buildNameItemKey( $name )
{
$normalized = trim( (string)$name );
if ( function_exists( 'mb_strtolower' ) )
$normalized = mb_strtolower( $normalized, 'UTF-8' );
else
$normalized = strtolower( $normalized );
return 'name:' . $normalized;
}
private function findItemMappingByExternalName( $itemName )
{
$itemName = trim( (string)$itemName );
if ( $itemName === '' )
return null;
$row = $this -> mdb -> query(
'SELECT * FROM fakturownia_item_mappings '
. 'WHERE LOWER( external_name ) = LOWER( :external_name ) '
. 'ORDER BY id DESC LIMIT 1',
[ ':external_name' => $itemName ]
) -> fetch( \PDO::FETCH_ASSOC );
return $row ? $row : null;
}
}