netgescon-day0/app/Services/Contabilita/MpsQifParser.php

162 lines
5.0 KiB
PHP

<?php
namespace App\Services\Contabilita;
use Carbon\Carbon;
class MpsQifParser
{
/**
* @return array{rows: array<int, array{data: Carbon, valuta: Carbon|null, descrizione: string, descrizione_estesa?: string|null, importo: float, causale: string|null, raw_line: string}>, meta: array<string,mixed>}
*/
public function parse(string $content): array
{
$content = str_replace(["\r\n", "\r"], "\n", $content);
$lines = array_values(array_filter(explode("\n", $content), static fn(string $line): bool => trim($line) !== ''));
if ($lines === []) {
throw new \RuntimeException('File QIF vuoto.');
}
$type = null;
$rows = [];
$transaction = [];
foreach ($lines as $line) {
$line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line;
$trimmed = trim($line);
if ($trimmed === '') {
continue;
}
if (str_starts_with($trimmed, '!Type:')) {
$type = substr($trimmed, 6) ?: null;
continue;
}
if ($trimmed === '^') {
$row = $this->buildRow($transaction, $type);
if ($row !== null) {
$rows[] = $row;
}
$transaction = [];
continue;
}
$code = substr($trimmed, 0, 1);
$value = trim(substr($trimmed, 1));
if (! isset($transaction[$code])) {
$transaction[$code] = [];
}
$transaction[$code][] = $value;
}
if ($transaction !== []) {
$row = $this->buildRow($transaction, $type);
if ($row !== null) {
$rows[] = $row;
}
}
if ($rows === []) {
throw new \RuntimeException('Nessun movimento valido trovato nel file QIF.');
}
return [
'rows' => $rows,
'meta' => [
'format' => 'mps_qif',
'qif_type' => $type,
],
];
}
/**
* @param array<string, array<int, string>> $transaction
* @return array{data: Carbon, valuta: Carbon|null, descrizione: string, descrizione_estesa?: string|null, importo: float, causale: string|null, raw_line: string}|null
*/
private function buildRow(array $transaction, ?string $type): ?array
{
$date = $this->parseDate($transaction['D'][0] ?? null);
$amount = $this->parseMoney($transaction['T'][0] ?? null);
if (! $date || $amount === null) {
return null;
}
$payee = $this->joinParts($transaction['P'] ?? []);
$memo = $this->joinParts($transaction['M'] ?? []);
$description = $payee !== '' ? $payee : ($memo !== '' ? mb_substr($memo, 0, 255) : 'Movimento QIF');
$extended = trim($payee . ($payee !== '' && $memo !== '' ? ' · ' : '') . $memo);
$rawLines = [];
foreach ($transaction as $code => $values) {
foreach ($values as $value) {
$rawLines[] = $code . $value;
}
}
return [
'data' => $date,
'valuta' => $date->copy(),
'descrizione' => $description,
'descrizione_estesa' => $extended !== '' ? $extended : null,
'importo' => $amount,
'causale' => $type ? ('QIF:' . strtoupper($type)) : 'QIF',
'raw_line' => implode("\n", $rawLines),
];
}
/**
* @param array<int, string> $parts
*/
private function joinParts(array $parts): string
{
$parts = array_map(static fn(string $value): string => trim($value), $parts);
$parts = array_values(array_filter($parts, static fn(string $value): bool => $value !== ''));
return trim(implode(' ', $parts));
}
private function parseDate(?string $value): ?Carbon
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
foreach (['d/m/Y', 'd/m/y', 'm/d/Y', 'm/d/y'] as $format) {
try {
return Carbon::createFromFormat($format, $value)->startOfDay();
} catch (\Throwable) {
// Try next format.
}
}
return null;
}
private function parseMoney(?string $value): ?float
{
$value = trim((string) $value);
if ($value === '') {
return null;
}
$value = str_replace(["\u{00A0}", "\t", ' '], '', $value);
$value = str_ireplace(['EUR', 'EURO', '€'], '', $value);
$value = preg_replace('/[^0-9,\.\-\+]/', '', $value) ?? $value;
if (str_contains($value, ',') && str_contains($value, '.')) {
$value = str_replace('.', '', $value);
$value = str_replace(',', '.', $value);
} elseif (str_contains($value, ',')) {
$value = str_replace(',', '.', $value);
}
return is_numeric($value) ? (float) $value : null;
}
}