-
Notifications
You must be signed in to change notification settings - Fork 59
Add credit card transaction retrieval (DKKKU) #574
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jwtue
wants to merge
15
commits into
nemiah:master
Choose a base branch
from
jwtue:feature/dkkku-credit-card-statements
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 13 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
53073fa
Add DKKKU credit card statement retrieval
jwtue 3269c92
Add the DIKKUS parameter DEG based on a real BW-Bank BPD
jwtue 613fb15
Discover credit card accounts from the UPD
jwtue 3573f13
Report the full account holder name for credit card accounts
jwtue 45afe9c
Call the credit card identifier an account number, not a card number
jwtue 9a4dce7
Correct the credit card record layout against real bank data
jwtue b5d8c8b
Add tests for the credit card segments and account discovery
jwtue 0ff8943
Test the pagination handling and document what remains unverified
jwtue e4847c5
Document credit card transactions and add a sample
jwtue 2e90eb6
Use the codebase's English term for the pagination token
jwtue df5e2b1
Add the missing entry count field to the DKKKU request
jwtue a4babd2
Warn about querying beyond the advertised retention window
jwtue 703aafc
Explain why the credit card segments link no specification
jwtue 881e9ad
Reject queries beyond the announced retention period
jwtue 76a8445
Limit the span of a request rather than how far back it reaches
jwtue File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| <?php | ||
|
|
||
| /** @noinspection PhpUnhandledExceptionInspection */ | ||
|
|
||
| /** | ||
| * SAMPLE - Displays the credit card transactions for a specific time range. | ||
| * | ||
| * Credit card accounts have no IBAN, so they are not part of getSEPAAccounts() and their transactions | ||
| * cannot be retrieved with GetStatementOfAccount. They use the DKKKU business transaction instead, | ||
| * which is defined by the Deutsche Kreditwirtschaft rather than by the FinTS specification, so not | ||
| * every bank offers it. | ||
| */ | ||
|
|
||
| // See login.php, it returns a FinTs instance that is already logged in. | ||
| /** @var \Fhp\FinTs $fints */ | ||
| $fints = require_once 'login.php'; | ||
|
|
||
| // The credit card accounts are described in the UPD, which arrived during login, so this needs no | ||
| // request to the bank at all. | ||
| $getCreditCardAccounts = \Fhp\Action\GetCreditCardAccounts::create(); | ||
| $fints->execute($getCreditCardAccounts); | ||
| $creditCardAccounts = $getCreditCardAccounts->getAccounts(); | ||
|
|
||
| if (count($creditCardAccounts) === 0) { | ||
| echo 'No credit card accounts found. Your bank may not support DKKKU.' . PHP_EOL; | ||
| return; | ||
| } | ||
|
|
||
| echo 'Credit card accounts:' . PHP_EOL; | ||
| foreach ($creditCardAccounts as $creditCardAccount) { | ||
| echo ' ' . $creditCardAccount->getAccountNumber() | ||
| . ' (' . $creditCardAccount->getProductName() . ')' . PHP_EOL; | ||
| } | ||
|
|
||
| // Just pick the first one, for demonstration purposes. | ||
| $oneAccount = $creditCardAccounts[0]; | ||
|
|
||
| $from = new \DateTime('-30 days'); | ||
| $to = new \DateTime(); | ||
| $getStatement = \Fhp\Action\GetCreditCardStatement::create($oneAccount, $from, $to); | ||
| $fints->execute($getStatement); | ||
| if ($getStatement->needsTan()) { | ||
| handleStrongAuthentication($getStatement); // See login.php for the implementation. | ||
| } | ||
|
|
||
| $statement = $getStatement->getStatement(); | ||
| if ($statement->getBalance() !== null) { | ||
| echo 'Balance: ' . $statement->getBalance() . ' ' . $statement->getBalanceCurrency() . PHP_EOL; | ||
| } | ||
| echo 'Transactions:' . PHP_EOL; | ||
| echo '=======================================' . PHP_EOL; | ||
| foreach ($statement->getTransactions() as $transaction) { | ||
| echo 'Booking date: ' . $transaction->getBookingDate()?->format('Y-m-d') . PHP_EOL; | ||
| echo 'Amount : ' . $transaction->getAmount() . ' ' . $transaction->getCurrency() . PHP_EOL; | ||
| // Transactions in a foreign currency also report what the merchant originally charged. | ||
| if ($transaction->getOriginalAmount() !== null) { | ||
| echo 'Original : ' . $transaction->getOriginalAmount() . ' ' . $transaction->getOriginalCurrency() | ||
| . ' (rate ' . $transaction->getExchangeRate() . ')' . PHP_EOL; | ||
| } | ||
| echo 'Purpose : ' . $transaction->getPurpose() . PHP_EOL; | ||
| echo 'Reference : ' . $transaction->getReference() . PHP_EOL; | ||
| echo 'Category : ' . ($transaction->getMerchantCategoryCode() ?? '-') . PHP_EOL; | ||
| echo '=======================================' . PHP_EOL . PHP_EOL; | ||
| } | ||
| echo 'Found ' . count($statement->getTransactions()) . ' transactions.' . PHP_EOL; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| <?php | ||
|
|
||
| namespace Fhp\Tests\Unit\Action; | ||
|
|
||
| use Fhp\Action\GetCreditCardAccounts; | ||
| use Fhp\Protocol\BPD; | ||
| use Fhp\Protocol\UPD; | ||
| use Fhp\Segment\BaseSegment; | ||
|
|
||
| /** | ||
| * Tests that credit card accounts are recognized in the UPD. | ||
| * | ||
| * The HIUPD segments below mirror the structure of a real BW-Bank/LBBW response, with all account | ||
| * numbers and names replaced. Note that the credit card account has no IBAN, which is why such | ||
| * accounts are absent from HKSPA and have to be found here instead. | ||
| */ | ||
| class GetCreditCardAccountsTest extends \PHPUnit\Framework\TestCase | ||
| { | ||
| /** Account type 50 (credit card), no IBAN, DKKKU among the permitted business transactions. */ | ||
| private const HIUPD_CREDIT_CARD = | ||
| 'HIUPD:12:6:4+5555000011112222::280:60050101++9999999999+50+EUR+Mustermann+Max+Example Goldcard Set++HKSAK:1+DKKKS:1+DKKKU:1+HKTAN:1\''; | ||
|
|
||
| /** An ordinary current account: has an IBAN, permits HKKAZ but not DKKKU. */ | ||
| private const HIUPD_CURRENT_ACCOUNT = | ||
| 'HIUPD:10:6:4+1234567890::280:60050101+DE02600501011234567890+9999999999+1+EUR+Mustermann+Max+Example Giro++HKSAK:1+HKKAZ:1+HKSAL:1\''; | ||
|
|
||
| /** A savings account, likewise without DKKKU. */ | ||
| private const HIUPD_SAVINGS_ACCOUNT = | ||
| 'HIUPD:11:6:4+2000130538::280:60050101+DE33600501012000130538+9999999999+10+EUR+Mustermann+Max+Example Sparkonto++HKSPA:1+HKKAZ:1\''; | ||
|
|
||
| /** @param string[] $rawHiupds */ | ||
| private static function runAction(array $rawHiupds): GetCreditCardAccounts | ||
| { | ||
| $upd = new UPD(); | ||
| $upd->hiupd = array_map(function (string $raw) { | ||
| return BaseSegment::parse($raw); | ||
| }, $rawHiupds); | ||
|
|
||
| $action = GetCreditCardAccounts::create(); | ||
| $action->getNextRequest(new BPD(), $upd); | ||
| return $action; | ||
| } | ||
|
|
||
| public function testFindsOnlyTheCreditCardAccount() | ||
| { | ||
| $action = self::runAction([ | ||
| self::HIUPD_CURRENT_ACCOUNT, | ||
| self::HIUPD_CREDIT_CARD, | ||
| self::HIUPD_SAVINGS_ACCOUNT, | ||
| ]); | ||
|
|
||
| $accounts = $action->getAccounts(); | ||
| $this->assertCount(1, $accounts); | ||
|
|
||
| $account = $accounts[0]; | ||
| $this->assertEquals('5555000011112222', $account->getAccountNumber()); | ||
| $this->assertEquals('60050101', $account->getBlz()); | ||
| $this->assertEquals(50, $account->getAccountType()); | ||
| $this->assertEquals('Example Goldcard Set', $account->getProductName()); | ||
| $this->assertEquals('EUR', $account->getCurrency()); | ||
| // Banks split the holder name across two fields. | ||
| $this->assertEquals('Mustermann Max', $account->getName()); | ||
| } | ||
|
|
||
| /** The data comes from the UPD, so no request to the bank is necessary. */ | ||
| public function testNeedsNoRequest() | ||
| { | ||
| $upd = new UPD(); | ||
| $upd->hiupd = [BaseSegment::parse(self::HIUPD_CREDIT_CARD)]; | ||
|
|
||
| $action = GetCreditCardAccounts::create(); | ||
| $requestSegments = $action->getNextRequest(new BPD(), $upd); | ||
|
|
||
| $this->assertEmpty($requestSegments); | ||
| $this->assertTrue($action->isDone()); | ||
| $this->assertCount(1, $action->getAccounts()); | ||
| } | ||
|
|
||
| public function testReturnsNothingWithoutCreditCardAccounts() | ||
| { | ||
| $action = self::runAction([self::HIUPD_CURRENT_ACCOUNT, self::HIUPD_SAVINGS_ACCOUNT]); | ||
| $this->assertCount(0, $action->getAccounts()); | ||
| } | ||
|
|
||
| /** | ||
| * HIUPD v4 does not report an account type at all, so the list of permitted business transactions | ||
| * is the only criterion that works across versions. | ||
| */ | ||
| public function testFindsAccountInOlderSegmentVersion() | ||
| { | ||
| $action = self::runAction([ | ||
| 'HIUPD:9:4:4+5555000011112244::280:60050101+9999999999+EUR+Mustermann+Erika+Legacy Card++DKKKU:1\'', | ||
| ]); | ||
|
|
||
| $accounts = $action->getAccounts(); | ||
| $this->assertCount(1, $accounts); | ||
| $this->assertEquals('5555000011112244', $accounts[0]->getAccountNumber()); | ||
| $this->assertNull($accounts[0]->getAccountType()); | ||
| } | ||
|
|
||
| /** If the bank sends no list of permitted transactions, fall back to the account type. */ | ||
| public function testFallsBackToAccountType() | ||
| { | ||
| $action = self::runAction([ | ||
| 'HIUPD:12:6:4+5555000011112255::280:60050101++9999999999+55+EUR+Mustermann+Max+Example Card\'', | ||
| ]); | ||
|
|
||
| $accounts = $action->getAccounts(); | ||
| $this->assertCount(1, $accounts); | ||
| $this->assertEquals(55, $accounts[0]->getAccountType()); | ||
| } | ||
|
|
||
| /** An explicit list that lacks DKKKU wins over the account type. */ | ||
| public function testAccountTypeDoesNotOverrideAnExplicitList() | ||
| { | ||
| $action = self::runAction([ | ||
| 'HIUPD:12:6:4+5555000011112266::280:60050101++9999999999+50+EUR+Mustermann+Max+Example Card++HKSAK:1+HKTAN:1\'', | ||
| ]); | ||
|
|
||
| $this->assertCount(0, $action->getAccounts()); | ||
| } | ||
|
|
||
| public function testRequiresUpd() | ||
| { | ||
| $this->expectException(\InvalidArgumentException::class); | ||
| GetCreditCardAccounts::create()->getNextRequest(new BPD(), null); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.