250 lines
7.3 KiB
PHP
250 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Import;
|
|
|
|
class OfxParser implements FileParserInterface
|
|
{
|
|
protected static array $supportedExtensions = ['ofx', 'qfx'];
|
|
|
|
/**
|
|
* Parse OFX file and return all transactions
|
|
*/
|
|
public function parse(string $filePath, array $options = []): array
|
|
{
|
|
$content = file_get_contents($filePath);
|
|
|
|
if ($content === false) {
|
|
throw new \RuntimeException("Could not read file: $filePath");
|
|
}
|
|
|
|
// Parse bank account info
|
|
$accountInfo = $this->parseAccountInfo($content);
|
|
|
|
// Parse transactions
|
|
$transactions = $this->parseTransactions($content);
|
|
|
|
// Formatar como dados tabulares
|
|
$headers = ['DTPOSTED', 'TRNTYPE', 'TRNAMT', 'FITID', 'NAME', 'MEMO'];
|
|
$data = [];
|
|
|
|
foreach ($transactions as $txn) {
|
|
$data[] = [
|
|
$txn['date'] ?? '',
|
|
$txn['type'] ?? '',
|
|
$txn['amount'] ?? '',
|
|
$txn['fitid'] ?? '',
|
|
$txn['name'] ?? '',
|
|
$txn['memo'] ?? '',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'headers' => $headers,
|
|
'data' => $data,
|
|
'total_rows' => count($data),
|
|
'account_info' => $accountInfo,
|
|
'raw_transactions' => $transactions,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Get headers (OFX has fixed structure)
|
|
*/
|
|
public function getHeaders(string $filePath, array $options = []): array
|
|
{
|
|
return ['DTPOSTED', 'TRNTYPE', 'TRNAMT', 'FITID', 'NAME', 'MEMO'];
|
|
}
|
|
|
|
/**
|
|
* Get preview data
|
|
*/
|
|
public function getPreview(string $filePath, int $rows = 10, array $options = []): array
|
|
{
|
|
$parsed = $this->parse($filePath, $options);
|
|
|
|
$preview = [];
|
|
$count = 0;
|
|
|
|
foreach ($parsed['data'] as $row) {
|
|
if ($count >= $rows) {
|
|
break;
|
|
}
|
|
$preview[] = [
|
|
'row_index' => $count,
|
|
'data' => $row,
|
|
];
|
|
$count++;
|
|
}
|
|
|
|
return [
|
|
'preview' => $preview,
|
|
'total_rows' => $parsed['total_rows'],
|
|
'columns_count' => count($parsed['headers']),
|
|
'headers' => $parsed['headers'],
|
|
'account_info' => $parsed['account_info'] ?? null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Parse account information from OFX
|
|
*/
|
|
protected function parseAccountInfo(string $content): array
|
|
{
|
|
$info = [];
|
|
|
|
// Bank ID
|
|
if (preg_match('/<BANKID>([^<\n]+)/i', $content, $matches)) {
|
|
$info['bank_id'] = trim($matches[1]);
|
|
}
|
|
|
|
// Account ID
|
|
if (preg_match('/<ACCTID>([^<\n]+)/i', $content, $matches)) {
|
|
$info['account_id'] = trim($matches[1]);
|
|
}
|
|
|
|
// Account Type
|
|
if (preg_match('/<ACCTTYPE>([^<\n]+)/i', $content, $matches)) {
|
|
$info['account_type'] = trim($matches[1]);
|
|
}
|
|
|
|
// Currency
|
|
if (preg_match('/<CURDEF>([^<\n]+)/i', $content, $matches)) {
|
|
$info['currency'] = trim($matches[1]);
|
|
}
|
|
|
|
// Balance
|
|
if (preg_match('/<BALAMT>([^<\n]+)/i', $content, $matches)) {
|
|
$info['balance'] = floatval(trim($matches[1]));
|
|
}
|
|
|
|
// Balance Date
|
|
if (preg_match('/<DTASOF>([^<\n]+)/i', $content, $matches)) {
|
|
$info['balance_date'] = $this->parseOfxDate(trim($matches[1]));
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Parse transactions from OFX
|
|
*/
|
|
protected function parseTransactions(string $content): array
|
|
{
|
|
$transactions = [];
|
|
|
|
// Find all STMTTRN blocks
|
|
preg_match_all('/<STMTTRN>(.*?)<\/STMTTRN>/is', $content, $matches);
|
|
|
|
// Também tentar sem tag de fechamento (OFX SGML)
|
|
if (empty($matches[1])) {
|
|
// Split by STMTTRN tags
|
|
$parts = preg_split('/<STMTTRN>/i', $content);
|
|
array_shift($parts); // Remover parte antes do primeiro STMTTRN
|
|
|
|
foreach ($parts as $part) {
|
|
// Encontrar fim da transação
|
|
$endPos = stripos($part, '</STMTTRN>');
|
|
if ($endPos !== false) {
|
|
$part = substr($part, 0, $endPos);
|
|
} else {
|
|
// Tentar encontrar próximo STMTTRN ou fim de lista
|
|
$nextPos = stripos($part, '<STMTTRN>');
|
|
if ($nextPos !== false) {
|
|
$part = substr($part, 0, $nextPos);
|
|
}
|
|
$endListPos = stripos($part, '</BANKTRANLIST>');
|
|
if ($endListPos !== false) {
|
|
$part = substr($part, 0, $endListPos);
|
|
}
|
|
}
|
|
|
|
$txn = $this->parseTransaction($part);
|
|
if (!empty($txn['amount'])) {
|
|
$transactions[] = $txn;
|
|
}
|
|
}
|
|
} else {
|
|
foreach ($matches[1] as $block) {
|
|
$txn = $this->parseTransaction($block);
|
|
if (!empty($txn['amount'])) {
|
|
$transactions[] = $txn;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $transactions;
|
|
}
|
|
|
|
/**
|
|
* Parse a single transaction block
|
|
*/
|
|
protected function parseTransaction(string $block): array
|
|
{
|
|
$txn = [];
|
|
|
|
// Type (CREDIT, DEBIT, etc.)
|
|
if (preg_match('/<TRNTYPE>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['type'] = trim($matches[1]);
|
|
}
|
|
|
|
// Date Posted
|
|
if (preg_match('/<DTPOSTED>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['date'] = $this->parseOfxDate(trim($matches[1]));
|
|
}
|
|
|
|
// Amount
|
|
if (preg_match('/<TRNAMT>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['amount'] = floatval(str_replace(',', '.', trim($matches[1])));
|
|
}
|
|
|
|
// FIT ID (unique identifier)
|
|
if (preg_match('/<FITID>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['fitid'] = trim($matches[1]);
|
|
}
|
|
|
|
// Name/Payee
|
|
if (preg_match('/<NAME>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['name'] = trim($matches[1]);
|
|
}
|
|
|
|
// Memo
|
|
if (preg_match('/<MEMO>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['memo'] = trim($matches[1]);
|
|
}
|
|
|
|
// Check Number
|
|
if (preg_match('/<CHECKNUM>([^<\n]+)/i', $block, $matches)) {
|
|
$txn['check_num'] = trim($matches[1]);
|
|
}
|
|
|
|
return $txn;
|
|
}
|
|
|
|
/**
|
|
* Parse OFX date format (YYYYMMDDHHMMSS)
|
|
*/
|
|
protected function parseOfxDate(string $date): string
|
|
{
|
|
// Remove timezone info
|
|
$date = preg_replace('/\[.*\]/', '', $date);
|
|
$date = trim($date);
|
|
|
|
if (strlen($date) >= 8) {
|
|
$year = substr($date, 0, 4);
|
|
$month = substr($date, 4, 2);
|
|
$day = substr($date, 6, 2);
|
|
return "$day/$month/$year";
|
|
}
|
|
|
|
return $date;
|
|
}
|
|
|
|
/**
|
|
* Check if parser supports the extension
|
|
*/
|
|
public static function supports(string $extension): bool
|
|
{
|
|
return in_array(strtolower($extension), self::$supportedExtensions);
|
|
}
|
|
}
|