Issues (5)

src/OFX.php (2 issues)

Labels
Severity
1
<?php
2
3
namespace Endeken\OFX;
4
5
use DateTime;
6
use DateTimeZone;
7
use Exception;
8
use SimpleXMLElement;
9
10
/**
11
 * Class OFX
12
 *
13
 * This class provides functions to parse OFX data and convert it into an associative array or appropriate objects.
14
 */
15
class OFX
16
{
17
    /**
18
     * Parse OFX data and return an associative array with the parsed information.
19
     *
20
     * @param string $ofxData The OFX data as a string.
21
     * @return OFXData|null An associative array with the parsed information or false on failure.
22
     * @throws Exception
23
     */
24
    public static function parse(string $ofxData): null|OFXData
25
    {
26
27
        // Check if SimpleXML object was created successfully
28
29
        $xml = OFXUtils::normalizeOfx($ofxData);
30
        if ($xml === false) {
31
            return null;
32
        }
33
34
        $signOn = self::parseSignOn($xml->SIGNONMSGSRSV1->SONRS);
35
        $accountInfo = self::parseAccountInfo($xml->SIGNUPMSGSRSV1->ACCTINFOTRNRS);
0 ignored issues
show
Are you sure the assignment to $accountInfo is correct as self::parseAccountInfo($...SGSRSV1->ACCTINFOTRNRS) targeting Endeken\OFX\OFX::parseAccountInfo() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
36
        $bankAccounts = [];
37
38
        if (isset($xml->BANKMSGSRSV1)) {
39
            foreach ($xml->BANKMSGSRSV1->STMTTRNRS as $accountStatement) {
40
                foreach ($accountStatement->STMTRS as $statementResponse) {
41
                    $bankAccounts[] = self::parseBankAccount($accountStatement->TRNUID, $statementResponse);
0 ignored issues
show
It seems like $statementResponse can also be of type null; however, parameter $xml of Endeken\OFX\OFX::parseBankAccount() does only seem to accept SimpleXMLElement, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

41
                    $bankAccounts[] = self::parseBankAccount($accountStatement->TRNUID, /** @scrutinizer ignore-type */ $statementResponse);
Loading history...
42
                }
43
            }
44
        } elseif (isset($xml->CREDITCARDMSGSRSV1)) {
45
            $bankAccounts[] = self::parseCreditAccount($xml->TRNUID, $xml->CREDITCARDMSGSRSV1->CCSTMTTRNRS);
46
        }
47
        return new OFXData($signOn, $accountInfo, $bankAccounts);
48
    }
49
50
    /**
51
     * @param SimpleXMLElement $xml
52
     * @return SignOn
53
     * @throws Exception
54
     */
55
    protected static function parseSignOn(SimpleXMLElement $xml): SignOn
56
    {
57
        $status = self::parseStatus($xml->STATUS);
58
        $dateTime = self::parseDate($xml->DTSERVER);
59
        $language = $xml->LANGUAGE;
60
        $institute = self::parseInstitute($xml->FI);
61
        return new SignOn($status, $dateTime, $language, $institute);
62
    }
63
64
    protected static function parseInstitute(SimpleXMLElement $xml): Institute
65
    {
66
        $name = (string) $xml->ORG;
67
        $id = (string) $xml->FID;
68
        return new Institute($id, $name);
69
    }
70
71
    /**
72
     * @param SimpleXMLElement $xml
73
     * @return Status
74
     */
75
    protected static function parseStatus(SimpleXMLElement $xml): Status
76
    {
77
        $code = (string) $xml->STATUS->CODE;
78
        $severity = (string) $xml->STATUS->SEVERITY;
79
        $message = (string) $xml->STATUS->MESSAGE;
80
        return new Status($code, $severity, $message);
81
    }
82
83
    /**
84
     * Parse a date string and return a formatted date.
85
     *
86
     * @param string $dateString The date string to parse.
87
     * @return DateTime The formatted date.
88
     * @throws Exception
89
     */
90
    protected static function parseDate(string $dateString): DateTime
91
    {
92
        $dateString = explode('.', $dateString)[0];
93
        // Extract the numeric part of the offset (e.g., -5 from [-5:EST])
94
        // Also deal with some OFX data where the date string contains the offset, but not the
95
        // timezone abbreviation, ie. 20240501094851[-8]
96
        preg_match('/([-+]\d+)(:)?(\w+)?/', $dateString, $matches);
97
98
        if (count($matches) >= 2) {
99
            $offset = $matches[1];
100
            $timezoneAbbreviation = $matches[3] ?? null;
101
102
            // Remove the offset with brackets and timezone abbreviation from the date string
103
            $dateStringWithoutOffset = preg_replace('/[-+]\d+:\w+/', '', $dateString);
104
105
            // Remove brackets and timezone abbreviation
106
            $dateStringWithoutOffset = str_replace(['[', ']', ':' . $timezoneAbbreviation], '', $dateStringWithoutOffset);
107
108
            // Create a DateTime object with the appropriate timezone offset
109
            $dateTime = new DateTime($dateStringWithoutOffset, new DateTimeZone("GMT$offset"));
110
            (null === $timezoneAbbreviation) ?: $dateTime->setTimezone(new DateTimeZone($timezoneAbbreviation));
111
112
        } else {
113
            // Handle cases where the date format doesn't match expectations
114
            // You might want to log an error or throw an exception depending on your needs
115
            $dateTime = new DateTime($dateString);
116
        }
117
        return $dateTime;
118
    }
119
120
    /**
121
     * @throws Exception
122
     */
123
    private static function parseBankAccount(string $uuid, SimpleXMLElement $xml): BankAccount
124
    {
125
        $accountNumber = $xml->BANKACCTFROM->ACCTID ?? 'N/A';
126
        $accountType = $xml->BANKACCTFROM->ACCTTYPE ?? 'N/A';
127
        $agencyNumber = $xml->BANKACCTFROM->BRANCHID ?? 'N/A';
128
        $routingNumber = $xml->BANKACCTFROM->BANKID ?? 'N/A';
129
        $balance = $xml->LEDGERBAL->BALAMT ?? 'N/A';
130
        $balanceDate =  (null !== $xml->LEDGERBAL->DTASOF) ? self::parseDate($xml->LEDGERBAL->DTASOF) : new DateTime();
131
        $statement = self::parseStatement($xml);
132
        return new BankAccount(
133
            $accountNumber,
134
            $accountType,
135
            $agencyNumber,
136
            $routingNumber,
137
            $balance,
138
            $balanceDate,
139
            $uuid,
140
            $statement,
141
        );
142
    }
143
144
    /**
145
     * @throws Exception
146
     */
147
    private static function parseCreditAccount(string $uuid, SimpleXMLElement $xml): BankAccount
148
    {
149
        $nodeName = 'CCACCTFROM';
150
        if (!isset($xml->CCSTMTRS->$nodeName)) {
151
            $nodeName = 'BANKACCTFROM';
152
        }
153
154
        $accountNumber = $xml->CCSTMTRS->$nodeName->ACCTID;
155
        $accountType = $xml->CCSTMTRS->$nodeName->ACCTTYPE;
156
        $agencyNumber = $xml->CCSTMTRS->$nodeName->BRANCHID;
157
        $routingNumber = $xml->CCSTMTRS->$nodeName->BANKID;
158
        $balance = $xml->CCSTMTRS->LEDGERBAL->BALAMT;
159
        $balanceDate = self::parseDate($xml->CCSTMTRS->LEDGERBAL->DTASOF);
160
        $statement = self::parseStatement($xml->CCSTMTRS);
161
        return new BankAccount(
162
            $accountNumber,
163
            $accountType,
164
            $agencyNumber,
165
            $routingNumber,
166
            $balance,
167
            $balanceDate,
168
            $uuid,
169
            $statement,
170
        );
171
    }
172
173
    /**
174
     * @throws Exception
175
     */
176
    private static function parseStatement(SimpleXMLElement $xml): Statement
177
    {
178
        $currency = $xml->CURDEF;
179
        $startDate = self::parseDate($xml->BANKTRANLIST->DTSTART);
180
        $endDate = self::parseDate($xml->BANKTRANLIST->DTEND);
181
        $transactions = [];
182
        foreach ($xml->BANKTRANLIST->STMTTRN as $transactionXml) {
183
            $type = (string) $transactionXml->TRNTYPE;
184
            $date = self::parseDate($transactionXml->DTPOSTED);
185
            $userInitiatedDate = null;
186
            if (!empty((string) $transactionXml->DTUSER)) {
187
                $userInitiatedDate = self::parseDate($transactionXml->DTUSER);
188
            }
189
            $amount = (float) $transactionXml->TRNAMT;
190
            $uniqueId = (string) $transactionXml->FITID;
191
            $name = (string) $transactionXml->NAME;
192
            $memo = (string) $transactionXml->MEMO;
193
            $sic = $transactionXml->SIC;
194
            $checkNumber = $transactionXml->CHECKNUM;
195
            $transactions[] = new Transaction(
196
                $type,
197
                $amount,
198
                $date,
199
                $userInitiatedDate,
200
                $uniqueId,
201
                $name,
202
                $memo,
203
                $sic,
204
                $checkNumber,
205
            );
206
        }
207
        return new Statement($currency, $transactions, $startDate, $endDate);
208
    }
209
210
    private static function parseAccountInfo(SimpleXMLElement $xml = null): array|null
211
    {
212
        if ($xml === null || !isset($xml->ACCTINFO)) {
213
            return null;
214
        }
215
        $accounts = [];
216
        foreach ($xml->ACCTINFO as $account) {
217
            $accounts[] = new AccountInfo(
218
                (string)$account->DESC,
219
                (string)$account->ACCTID
220
            );
221
        }
222
223
        return $accounts;
224
    }
225
}
226