webmoney/backend/app/Services/Import/OfxParser.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);
}
}