Skip to content
This repository has been archived by the owner on Sep 8, 2020. It is now read-only.

Handle the issue with FNB's multiple elements on a single line which … #35

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
49 changes: 33 additions & 16 deletions lib/OfxParser/Ofx.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,32 +147,47 @@ private function buildBankAccounts(SimpleXMLElement $xml)
// Loop through the bank accounts
$bankAccounts = [];
foreach ($xml->BANKMSGSRSV1->STMTTRNRS as $accountStatement) {
$bankAccounts[] = $this->buildBankAccount($accountStatement);
foreach ($accountStatement->STMTRS as $statementResponse) {
$bankAccounts[] = $this->buildBankAccount($accountStatement->TRNUID, $statementResponse);
}
}
return $bankAccounts;
}

/**
* @param SimpleXMLElement $xml
* @param string $transactionUid
* @param SimpleXMLElement $statementResponse
* @return BankAccount
* @throws \Exception
*/
private function buildBankAccount(SimpleXMLElement $xml)
private function buildBankAccount($transactionUid, SimpleXMLElement $statementResponse)
{
$bankAccount = new BankAccount();
$bankAccount->transactionUid = $xml->TRNUID;
$bankAccount->agencyNumber = $xml->STMTRS->BANKACCTFROM->BRANCHID;
$bankAccount->accountNumber = $xml->STMTRS->BANKACCTFROM->ACCTID;
$bankAccount->routingNumber = $xml->STMTRS->BANKACCTFROM->BANKID;
$bankAccount->accountType = $xml->STMTRS->BANKACCTFROM->ACCTTYPE;
$bankAccount->balance = $xml->STMTRS->LEDGERBAL->BALAMT;
$bankAccount->balanceDate = $this->createDateTimeFromStr($xml->STMTRS->LEDGERBAL->DTASOF, true);
$bankAccount->transactionUid = $transactionUid;
$bankAccount->agencyNumber = $statementResponse->BANKACCTFROM->BRANCHID;
$bankAccount->accountNumber = $statementResponse->BANKACCTFROM->ACCTID;
$bankAccount->routingNumber = $statementResponse->BANKACCTFROM->BANKID;
$bankAccount->accountType = $statementResponse->BANKACCTFROM->ACCTTYPE;
$bankAccount->balance = $statementResponse->LEDGERBAL->BALAMT;
$bankAccount->balanceDate = $this->createDateTimeFromStr(
$statementResponse->LEDGERBAL->DTASOF,
true
);

$bankAccount->statement = new Statement();
$bankAccount->statement->currency = $xml->STMTRS->CURDEF;
$bankAccount->statement->startDate = $this->createDateTimeFromStr($xml->STMTRS->BANKTRANLIST->DTSTART);
$bankAccount->statement->endDate = $this->createDateTimeFromStr($xml->STMTRS->BANKTRANLIST->DTEND);
$bankAccount->statement->transactions = $this->buildTransactions($xml->STMTRS->BANKTRANLIST->STMTTRN);
$bankAccount->statement->currency = $statementResponse->CURDEF;

$bankAccount->statement->startDate = $this->createDateTimeFromStr(
$statementResponse->BANKTRANLIST->DTSTART
);

$bankAccount->statement->endDate = $this->createDateTimeFromStr(
$statementResponse->BANKTRANLIST->DTEND
);

$bankAccount->statement->transactions = $this->buildTransactions(
$statementResponse->BANKTRANLIST->STMTTRN
);

return $bankAccount;
}
Expand Down Expand Up @@ -264,6 +279,8 @@ private function buildStatus(SimpleXMLElement $xml)
*/
private function createDateTimeFromStr($dateString, $ignoreErrors = false)
{
if((!isset($dateString) || trim($dateString) === '')) return null;

$regex = '/'
. "(\d{4})(\d{2})(\d{2})?" // YYYYMMDD 1,2,3
. "(?:(\d{2})(\d{2})(\d{2}))?" // HHMMSS - optional 4,5,6
Expand Down Expand Up @@ -310,7 +327,7 @@ private function createDateTimeFromStr($dateString, $ignoreErrors = false)
private function createAmountFromStr($amountString)
{
// Decimal mark style (UK/US): 000.00 or 0,000.00
if (preg_match('/^-?([\d,]+)(\.?)([\d]{2})$/', $amountString) === 1) {
if (preg_match('/^(-|\+)?([\d,]+)(\.?)([\d]{2})$/', $amountString) === 1) {
return (float)preg_replace(
['/([,]+)/', '/\.?([\d]{2})$/'],
['', '.$1'],
Expand All @@ -319,7 +336,7 @@ private function createAmountFromStr($amountString)
}

// European style: 000,00 or 0.000,00
if (preg_match('/^-?([\d\.]+,?[\d]{2})$/', $amountString) === 1) {
if (preg_match('/^(-|\+)?([\d\.]+,?[\d]{2})$/', $amountString) === 1) {
return (float)preg_replace(
['/([\.]+)/', '/,?([\d]{2})$/'],
['', '.$1'],
Expand Down
8 changes: 7 additions & 1 deletion lib/OfxParser/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ private function conditionallyAddNewlines($ofxContent)
return str_replace('<', "\n<", $ofxContent); // add line breaks to allow XML to parse
}

if (preg_match('/<.+>.*<.+>/', $ofxContent) === 1) {
return str_replace('<', "\n<", $ofxContent); // add line breaks to allow XML to parse
}

return $ofxContent;
}

Expand Down Expand Up @@ -98,7 +102,7 @@ private function closeUnclosedXmlTags($line)
// Does not match: <SOMETHING>
// Does not match: <SOMETHING>blah</SOMETHING>
if (preg_match(
"/<([A-Za-z0-9.]+)>([\wà-úÀ-Ú0-9\.\-\_\+\, ;:\[\]\'\&\/\\\*\(\)\+\{\|\}\!\£\$\?=@€£#%±§~`]+)$/",
"/<([A-Za-z0-9.]+)>([\wà-úÀ-Ú0-9\.\-\_\+\, ;:\[\]\'\&\/\\\*\(\)\+\{\|\}\!\£\$\?=@€£#%±§~`\"]+)$/",
trim($line),
$matches
)) {
Expand All @@ -117,6 +121,8 @@ private function convertSgmlToXml($sgml)
{
$sgml = str_replace(["\r\n", "\r"], "\n", $sgml);

$sgml = preg_replace('/&(?!#?[a-z0-9]+;)/', '&amp;', $sgml);

$lines = explode("\n", $sgml);

$xml = '';
Expand Down
126 changes: 126 additions & 0 deletions lib/OfxParser/Parserv2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace OfxParser;

/**
* An OFX parser library
*
* Heavily refactored from Guillaume Bailleul's grimfor/ofxparser
*
* @author Guillaume BAILLEUL <[email protected]>
* @author James Titcumb <[email protected]>
* @author Oliver Lowe <[email protected]>
*/
class Parserv2
{
/**
* Load an OFX file into this parser by way of a filename
*
* @param string $ofxFile A path that can be loaded with file_get_contents
* @return Ofx
* @throws \Exception
*/
public function loadFromFile($ofxFile)
{
if (!file_exists($ofxFile)) {
throw new \InvalidArgumentException("File '{$ofxFile}' could not be found");
}

return $this->loadFromString(file_get_contents($ofxFile));
}

/**
* Load an OFX by directly using the text content
*
* @param string $ofxContent
* @return Ofx
* @throws \Exception
*/
public function loadFromString($ofxContent)
{
$ofxContent = utf8_encode($ofxContent);
$xml = $this->xmlLoadString($ofxContent);

return new Ofx($xml);
}

/**
* Detect if the OFX file is on one line. If it is, add newlines automatically.
*
* @param string $ofxContent
* @return string
*/
private function conditionallyAddNewlines($ofxContent)
{
if (preg_match('/<OFX>.*<\/OFX>/', $ofxContent) === 1) {
return str_replace('<', "\n<", $ofxContent); // add line breaks to allow XML to parse
}

if (preg_match('/<.+>.*<.+>/', $ofxContent) === 1) {
return str_replace('<', "\n<", $ofxContent); // add line breaks to allow XML to parse
}

return $ofxContent;
}

/**
* Load an XML string without PHP errors - throws exception instead
*
* @param string $xmlString
* @throws \Exception
* @return \SimpleXMLElement
*/
private function xmlLoadString($xmlString)
{
libxml_clear_errors();
libxml_use_internal_errors(true);
$xml = simplexml_load_string($xmlString);

if ($errors = libxml_get_errors()) {
throw new \RuntimeException('Failed to parse OFX: ' . var_export($errors, true));
}

return $xml;
}

/**
* Detect any unclosed XML tags - if they exist, close them
*
* @param string $line
* @return string
*/
private function closeUnclosedXmlTags($line)
{
// Matches: <SOMETHING>blah
// Does not match: <SOMETHING>
// Does not match: <SOMETHING>blah</SOMETHING>
if (preg_match(
"/<([A-Za-z0-9.]+)>([\wà-úÀ-Ú0-9\.\-\_\+\, ;:\[\]\'\&\/\\\*\(\)\+\{\|\}\!\£\$\?=@€£#%±§~`]+)$/",
trim($line),
$matches
)) {
return "<{$matches[1]}>{$matches[2]}</{$matches[1]}>";
}
return $line;
}

/**
* Convert an SGML to an XML string
*
* @param string $sgml
* @return string
*/
private function convertSgmlToXml($sgml)
{
$sgml = str_replace(["\r\n", "\r"], "\n", $sgml);

$lines = explode("\n", $sgml);

$xml = '';
foreach ($lines as $line) {
$xml .= trim($this->closeUnclosedXmlTags($line)) . "\n";
}

return trim($xml);
}
}
11 changes: 11 additions & 0 deletions tests/OfxParser/OfxTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public function amountConversionProvider()
'1' => ['1', 1.0],
'10' => ['10', 10.0],
'100' => ['100', 1.0], // @todo this is weird behaviour, should not really expect this
'+1' => ['+1', 1.0],
'+10' => ['+10', 10.0],
'+1000.00' => ['+1000.00', 1000.0],
'+1000,00' => ['+1000,00', 1000.0],
'+1,000.00' => ['+1,000.00', 1000.0],
'+1.000,00' => ['+1.000,00', 1000.0],
];
}

Expand Down Expand Up @@ -81,6 +87,11 @@ public function testCreateDateTimeFromOFXDateFormats()
// Test YYYYMMDDHHMMSS.XXX
$DateTimeFour = $method->invoke($Ofx, '20081005132200.124');
self::assertEquals($expectedDateTime->getTimestamp(), $DateTimeFour->getTimestamp());

// Test empty datetime
$DateTimeFour = $method->invoke($Ofx, '');
self::assertEquals(null, $DateTimeFour);

}

public function testBuildsSignOn()
Expand Down
16 changes: 16 additions & 0 deletions tests/OfxParser/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ public function convertSgmlToXmlProvider()
<ACCTID>XXXXXXXXXXX</ACCTID>
<ACCTTYPE>CHECKING</ACCTTYPE>
</BANKACCTFROM>
HERE
],[<<<HERE
<SOMETHING>
<FOO>bar & restaurant
<BAZ>bat</BAZ>
</SOMETHING>
HERE
, <<<HERE
<SOMETHING>
<FOO>bar &amp; restaurant</FOO>
<BAZ>bat</BAZ>
</SOMETHING>
HERE
],
];
Expand Down Expand Up @@ -163,8 +175,12 @@ public function loadFromStringProvider()
'ofxdata-oneline.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-oneline.ofx'],
'ofxdata-cmfr.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-cmfr.ofx'],
'ofxdata-bb.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-bb.ofx'],
'ofxdata-bb-two-stmtrs.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-bb-two-stmtrs.ofx'],
'ofxdata-credit-card.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-credit-card.ofx'],
'ofxdata-bpbfc.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-bpbfc.ofx'],
'ofxdata-memoWithQuotes.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-memoWithQuotes.ofx'],
'ofxdata-emptyDateTime.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-emptyDateTime.ofx'],
'ofxdata-memoWithAmpersand.ofx' => [dirname(__DIR__).'/fixtures/ofxdata-memoWithAmpersand.ofx'],
];
}

Expand Down
Loading